diff --git a/crates/bindings-macro/src/view.rs b/crates/bindings-macro/src/view.rs index 63bed482f74..9090ee97e5c 100644 --- a/crates/bindings-macro/src/view.rs +++ b/crates/bindings-macro/src/view.rs @@ -12,15 +12,38 @@ use crate::util::{check_duplicate_msg, match_meta}; pub(crate) struct ViewArgs { name: Option, accessor: Ident, + primary_key: Option, #[allow(unused)] public: bool, } +enum ViewPrimaryKeyArg { + Ident(Ident), + Literal(LitStr), +} + +impl ViewPrimaryKeyArg { + 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 +59,15 @@ 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")?; + let value = meta.value()?; + primary_key = Some(if value.peek(LitStr) { + ViewPrimaryKeyArg::Literal(value.parse()?) + } else { + ViewPrimaryKeyArg::Ident(value.parse()?) + }); + } }); Ok(()) }) @@ -51,6 +83,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 +107,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 +277,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 +317,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 +342,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..b68ba7a0bd9 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 ValidateViewReturnPrimaryKey, type Views, } from './views'; import type { UntypedTableDef } from '../lib/table'; @@ -347,7 +348,8 @@ export class Schema implements ModuleDefaultExport { view>( opts: ViewOpts, ret: Ret, - fn: F + fn: F, + ..._: ValidateViewReturnPrimaryKey ): ViewExport { return makeViewExport(this.#ctx, opts, {}, ret, fn); } @@ -380,7 +382,12 @@ 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, + ..._: ValidateViewReturnPrimaryKey + ): 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..ec3245b2fbd 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 multiplePrimaryKeyViewRows = t.array( + t.row('MultiplePrimaryKeyViewRows', { + 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: 'multiplePrimaryKeyViewRows', public: true }, + multiplePrimaryKeyViewRows, + () => [] +); + 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..26115b44209 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,57 @@ export type ViewOpts = { type FlattenedArray = T extends readonly (infer E)[] ? E : never; +type ViewReturnRow = + Ret extends ArrayBuilder + ? Element extends RowBuilder + ? Row + : never + : Ret extends OptionBuilder + ? Value extends RowBuilder + ? Row + : never + : never; + +type PrimaryKeyColumnNames = { + [K in keyof Row & string]: Row[K] extends ColumnBuilder + ? M extends { isPrimaryKey: true } + ? K + : never + : never; +}[keyof Row & string]; + +type IsUnion = [T] extends [never] + ? false + : T extends any + ? [U] extends [T] + ? false + : true + : false; + +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'; +}; + +export type ValidateViewReturnPrimaryKey = + 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 +219,19 @@ export function registerView< returnType, }); + 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 +262,34 @@ export function registerView< }); } +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); +} + +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..ad4c0f88292 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, @@ -575,6 +575,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 +583,23 @@ impl From for RawModuleDefV10 { RawIdentifier::from(vd.accessor_name.clone()), RawIdentifier::from(vd.name.clone()), ); + if 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()), + columns: vec![RawIdentifier::from(column.accessor_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)); diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index e840770a8c2..11077def508 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,65 @@ fn attach_schedules_to_tables( Ok(()) } +fn attach_view_primary_keys( + views: &mut IndexMap, + primary_keys: Vec, +) -> Result<()> { + let mut errors = Vec::new(); + let mut seen = Vec::new(); + + for primary_key in primary_keys { + let RawViewPrimaryKeyDefV10 { + view_source_name, + columns, + } = primary_key; + + if seen.contains(&view_source_name) { + errors.push(ValidationError::RepeatedViewPrimaryKey { + view: view_source_name.clone(), + }); + continue; + } + seen.push(view_source_name.clone()); + + 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; + }; + + 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; + }; + + 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; + }; + + 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 +920,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..502fdc9f6c1 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("View {view} primary key column {column} 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 6549919551d..66edff74429 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..817a03250fb --- /dev/null +++ b/crates/smoketests/modules/views-primary-key-auto-migrate-updated/src/lib.rs @@ -0,0 +1,25 @@ +use spacetimedb::{log, ReducerContext, Table, ViewContext}; + +#[spacetimedb::table(accessor = player_state, public)] +pub struct PlayerState { + #[primary_key] + id: u64, + #[index(btree)] + level: u64, +} + +#[spacetimedb::reducer] +pub fn set_player_state(ctx: &ReducerContext, id: u64, level: u64) { + ctx.db.player_state().id().delete(&id); + ctx.db.player_state().insert(PlayerState { id, level }); +} + +#[spacetimedb::reducer(client_disconnected)] +pub fn identity_disconnected(_ctx: &ReducerContext) { + log::info!("VIEW_PK_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..1547bb9c5ad --- /dev/null +++ b/crates/smoketests/modules/views-primary-key-auto-migrate/src/lib.rs @@ -0,0 +1,20 @@ +use spacetimedb::{ReducerContext, Table, ViewContext}; + +#[spacetimedb::table(accessor = player_state, public)] +pub struct PlayerState { + #[primary_key] + id: u64, + #[index(btree)] + level: u64, +} + +#[spacetimedb::reducer] +pub fn set_player_state(ctx: &ReducerContext, id: u64, level: u64) { + ctx.db.player_state().id().delete(&id); + ctx.db.player_state().insert(PlayerState { id, level }); +} + +#[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..d8121e6805f 100644 --- a/crates/smoketests/tests/smoketests/views.rs +++ b/crates/smoketests/tests/smoketests/views.rs @@ -508,6 +508,43 @@ 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.call("set_player_state", &["1", "1"]).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(); + + let events = sub.collect().unwrap(); + assert_eq!( + serde_json::json!(project_fields(events, "player", &["id", "level"])), + serde_json::json!([ + {"player": {"deletes": [], "inserts": [{"id": 1, "level": 1}]}} + ]) + ); + + 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_PK_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-view-pk/src/lib.rs b/modules/sdk-test-view-pk/src/lib.rs index f9e886f3b3a..331e1cafd70 100644 --- a/modules/sdk-test-view-pk/src/lib.rs +++ b/modules/sdk-test-view-pk/src/lib.rs @@ -1,4 +1,4 @@ -use spacetimedb::{reducer, table, view, Query, ReducerContext, Table, ViewContext}; +use spacetimedb::{reducer, table, view, Identity, Query, ReducerContext, SpacetimeType, Table, ViewContext}; #[table(accessor = view_pk_player, public)] pub struct ViewPkPlayer { @@ -23,6 +23,31 @@ pub struct ViewPkMembershipSecondary { pub player_id: u64, } +#[derive(Clone, SpacetimeType)] +pub struct ProceduralViewPkPlayer { + pub id: u64, + pub name: String, +} + +#[table(accessor = procedural_view_pk_player_source, public)] +pub struct ProceduralViewPkPlayerSource { + #[primary_key] + pub record_id: u64, + #[index(btree)] + pub id: u64, + pub name: String, +} + +#[table(accessor = sender_procedural_view_pk_player_source, public)] +pub struct SenderProceduralViewPkPlayerSource { + #[primary_key] + pub record_id: u64, + #[index(btree)] + pub owner: Identity, + pub id: u64, + pub name: String, +} + #[reducer] pub fn insert_view_pk_player(ctx: &ReducerContext, id: u64, name: String) { ctx.db.view_pk_player().insert(ViewPkPlayer { id, name }); @@ -45,11 +70,78 @@ pub fn insert_view_pk_membership_secondary(ctx: &ReducerContext, id: u64, player .insert(ViewPkMembershipSecondary { id, player_id }); } +#[reducer] +pub fn insert_procedural_view_pk_player(ctx: &ReducerContext, record_id: u64, id: u64, name: String) { + ctx.db + .procedural_view_pk_player_source() + .insert(ProceduralViewPkPlayerSource { record_id, id, name }); +} + +#[reducer] +pub fn update_procedural_view_pk_player(ctx: &ReducerContext, record_id: u64, id: u64, name: String) { + ctx.db + .procedural_view_pk_player_source() + .record_id() + .update(ProceduralViewPkPlayerSource { record_id, id, name }); +} + +#[reducer] +pub fn insert_sender_procedural_view_pk_player(ctx: &ReducerContext, record_id: u64, id: u64, name: String) { + ctx.db + .sender_procedural_view_pk_player_source() + .insert(SenderProceduralViewPkPlayerSource { + record_id, + owner: ctx.sender(), + id, + name, + }); +} + +#[reducer] +pub fn update_sender_procedural_view_pk_player(ctx: &ReducerContext, record_id: u64, id: u64, name: String) { + ctx.db + .sender_procedural_view_pk_player_source() + .record_id() + .update(SenderProceduralViewPkPlayerSource { + record_id, + owner: ctx.sender(), + id, + name, + }); +} + #[view(accessor = all_view_pk_players, public)] pub fn all_view_pk_players(ctx: &ViewContext) -> impl Query { ctx.from.view_pk_player() } +#[view(accessor = all_procedural_view_pk_players, public, primary_key = id)] +pub fn all_procedural_view_pk_players(ctx: &ViewContext) -> Vec { + ctx.db + .procedural_view_pk_player_source() + .id() + .filter(0u64..) + .map(|row| ProceduralViewPkPlayer { + id: row.id, + name: row.name, + }) + .collect() +} + +#[view(accessor = sender_procedural_view_pk_players, public, primary_key = id)] +pub fn sender_procedural_view_pk_players(ctx: &ViewContext) -> Vec { + let sender = ctx.sender(); + ctx.db + .sender_procedural_view_pk_player_source() + .owner() + .filter(sender) + .map(|row| ProceduralViewPkPlayer { + id: row.id, + name: row.name, + }) + .collect() +} + #[view(accessor = sender_view_pk_players_a, public)] pub fn sender_view_pk_players_a(ctx: &ViewContext) -> impl Query { ctx.from diff --git a/sdks/rust/tests/test.rs b/sdks/rust/tests/test.rs index c487661b67d..d8fa5aad621 100644 --- a/sdks/rust/tests/test.rs +++ b/sdks/rust/tests/test.rs @@ -768,3 +768,29 @@ macro_rules! view_pk_tests { view_pk_tests!(rust_view_pk, ""); view_pk_tests!(csharp_view_pk, "-cs"); + +mod rust_procedural_view_pk { + use spacetimedb_testing::sdk::Test; + + const MODULE: &str = "sdk-test-view-pk"; + const CLIENT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/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 procedural_view_with_pk_on_update_callback() { + make_test("procedural-view-pk-on-update").run() + } + + #[test] + fn sender_procedural_view_with_pk_scopes_updates_by_sender() { + make_test("sender-procedural-view-pk-updates-are-scoped").run() + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/all_procedural_view_pk_players_table.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/all_procedural_view_pk_players_table.rs new file mode 100644 index 00000000000..4173e9c5902 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/all_procedural_view_pk_players_table.rs @@ -0,0 +1,161 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::procedural_view_pk_player_type::ProceduralViewPkPlayer; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `all_procedural_view_pk_players`. +/// +/// Obtain a handle from the [`AllProceduralViewPkPlayersTableAccess::all_procedural_view_pk_players`] method on [`super::RemoteTables`], +/// like `ctx.db.all_procedural_view_pk_players()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.all_procedural_view_pk_players().on_insert(...)`. +pub struct AllProceduralViewPkPlayersTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `all_procedural_view_pk_players`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait AllProceduralViewPkPlayersTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`AllProceduralViewPkPlayersTableHandle`], which mediates access to the table `all_procedural_view_pk_players`. + fn all_procedural_view_pk_players(&self) -> AllProceduralViewPkPlayersTableHandle<'_>; +} + +impl AllProceduralViewPkPlayersTableAccess for super::RemoteTables { + fn all_procedural_view_pk_players(&self) -> AllProceduralViewPkPlayersTableHandle<'_> { + AllProceduralViewPkPlayersTableHandle { + imp: self + .imp + .get_table::("all_procedural_view_pk_players"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct AllProceduralViewPkPlayersInsertCallbackId(__sdk::CallbackId); +pub struct AllProceduralViewPkPlayersDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for AllProceduralViewPkPlayersTableHandle<'ctx> { + type Row = ProceduralViewPkPlayer; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = AllProceduralViewPkPlayersInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> AllProceduralViewPkPlayersInsertCallbackId { + AllProceduralViewPkPlayersInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: AllProceduralViewPkPlayersInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = AllProceduralViewPkPlayersDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> AllProceduralViewPkPlayersDeleteCallbackId { + AllProceduralViewPkPlayersDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: AllProceduralViewPkPlayersDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct AllProceduralViewPkPlayersUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for AllProceduralViewPkPlayersTableHandle<'ctx> { + type UpdateCallbackId = AllProceduralViewPkPlayersUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> AllProceduralViewPkPlayersUpdateCallbackId { + AllProceduralViewPkPlayersUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: AllProceduralViewPkPlayersUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `id` unique index on the table `all_procedural_view_pk_players`, +/// which allows point queries on the field of the same name +/// via the [`AllProceduralViewPkPlayersIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.all_procedural_view_pk_players().id().find(...)`. +pub struct AllProceduralViewPkPlayersIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> AllProceduralViewPkPlayersTableHandle<'ctx> { + /// Get a handle on the `id` unique index on the table `all_procedural_view_pk_players`. + pub fn id(&self) -> AllProceduralViewPkPlayersIdUnique<'ctx> { + AllProceduralViewPkPlayersIdUnique { + imp: self.imp.get_unique_constraint::("id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> AllProceduralViewPkPlayersIdUnique<'ctx> { + /// Find the subscribed row whose `id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &u64) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("all_procedural_view_pk_players"); + _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 `ProceduralViewPkPlayer`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait all_procedural_view_pk_playersQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `ProceduralViewPkPlayer`. + fn all_procedural_view_pk_players(&self) -> __sdk::__query_builder::Table; +} + +impl all_procedural_view_pk_playersQueryTableAccess for __sdk::QueryTableAccessor { + fn all_procedural_view_pk_players(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("all_procedural_view_pk_players") + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/insert_procedural_view_pk_player_reducer.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_procedural_view_pk_player_reducer.rs new file mode 100644 index 00000000000..b242bf76d9e --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_procedural_view_pk_player_reducer.rs @@ -0,0 +1,76 @@ +// 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 InsertProceduralViewPkPlayerArgs { + pub record_id: u64, + pub id: u64, + pub name: String, +} + +impl From for super::Reducer { + fn from(args: InsertProceduralViewPkPlayerArgs) -> Self { + Self::InsertProceduralViewPkPlayer { + record_id: args.record_id, + id: args.id, + name: args.name, + } + } +} + +impl __sdk::InModule for InsertProceduralViewPkPlayerArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `insert_procedural_view_pk_player`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait insert_procedural_view_pk_player { + /// Request that the remote module invoke the reducer `insert_procedural_view_pk_player` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and this method provides no way to listen for its completion status. + /// /// Use [`insert_procedural_view_pk_player:insert_procedural_view_pk_player_then`] to run a callback after the reducer completes. + fn insert_procedural_view_pk_player(&self, record_id: u64, id: u64, name: String) -> __sdk::Result<()> { + self.insert_procedural_view_pk_player_then(record_id, id, name, |_, _| {}) + } + + /// Request that the remote module invoke the reducer `insert_procedural_view_pk_player` to run as soon as possible, + /// registering `callback` to run when we are notified that the reducer completed. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed with the `callback`. + fn insert_procedural_view_pk_player_then( + &self, + record_id: u64, + id: u64, + name: String, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()>; +} + +impl insert_procedural_view_pk_player for super::RemoteReducers { + fn insert_procedural_view_pk_player_then( + &self, + record_id: u64, + id: u64, + name: String, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()> { + self.imp + .invoke_reducer_with_callback(InsertProceduralViewPkPlayerArgs { record_id, id, name }, callback) + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/insert_sender_procedural_view_pk_player_reducer.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_sender_procedural_view_pk_player_reducer.rs new file mode 100644 index 00000000000..23875fbb01d --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/insert_sender_procedural_view_pk_player_reducer.rs @@ -0,0 +1,76 @@ +// 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 InsertSenderProceduralViewPkPlayerArgs { + pub record_id: u64, + pub id: u64, + pub name: String, +} + +impl From for super::Reducer { + fn from(args: InsertSenderProceduralViewPkPlayerArgs) -> Self { + Self::InsertSenderProceduralViewPkPlayer { + record_id: args.record_id, + id: args.id, + name: args.name, + } + } +} + +impl __sdk::InModule for InsertSenderProceduralViewPkPlayerArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `insert_sender_procedural_view_pk_player`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait insert_sender_procedural_view_pk_player { + /// Request that the remote module invoke the reducer `insert_sender_procedural_view_pk_player` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and this method provides no way to listen for its completion status. + /// /// Use [`insert_sender_procedural_view_pk_player:insert_sender_procedural_view_pk_player_then`] to run a callback after the reducer completes. + fn insert_sender_procedural_view_pk_player(&self, record_id: u64, id: u64, name: String) -> __sdk::Result<()> { + self.insert_sender_procedural_view_pk_player_then(record_id, id, name, |_, _| {}) + } + + /// Request that the remote module invoke the reducer `insert_sender_procedural_view_pk_player` to run as soon as possible, + /// registering `callback` to run when we are notified that the reducer completed. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed with the `callback`. + fn insert_sender_procedural_view_pk_player_then( + &self, + record_id: u64, + id: u64, + name: String, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()>; +} + +impl insert_sender_procedural_view_pk_player for super::RemoteReducers { + fn insert_sender_procedural_view_pk_player_then( + &self, + record_id: u64, + id: u64, + name: String, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()> { + self.imp + .invoke_reducer_with_callback(InsertSenderProceduralViewPkPlayerArgs { record_id, id, name }, callback) + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/mod.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/mod.rs index 04ee8aedb08..a5c6662e8be 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,17 +1,28 @@ // 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.2.0 (commit 29de10d9c443a1dcabb2ba897bc25f61b6c9bf7d). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +pub mod all_procedural_view_pk_players_table; pub mod all_view_pk_players_table; +pub mod insert_procedural_view_pk_player_reducer; +pub mod insert_sender_procedural_view_pk_player_reducer; pub mod insert_view_pk_membership_reducer; pub mod insert_view_pk_membership_secondary_reducer; pub mod insert_view_pk_player_reducer; +pub mod procedural_view_pk_player_source_table; +pub mod procedural_view_pk_player_source_type; +pub mod procedural_view_pk_player_type; +pub mod sender_procedural_view_pk_player_source_table; +pub mod sender_procedural_view_pk_player_source_type; +pub mod sender_procedural_view_pk_players_table; pub mod sender_view_pk_players_a_table; pub mod sender_view_pk_players_b_table; +pub mod update_procedural_view_pk_player_reducer; +pub mod update_sender_procedural_view_pk_player_reducer; pub mod update_view_pk_player_reducer; pub mod view_pk_membership_secondary_table; pub mod view_pk_membership_secondary_type; @@ -20,12 +31,23 @@ pub mod view_pk_membership_type; pub mod view_pk_player_table; pub mod view_pk_player_type; +pub use all_procedural_view_pk_players_table::*; pub use all_view_pk_players_table::*; +pub use insert_procedural_view_pk_player_reducer::insert_procedural_view_pk_player; +pub use insert_sender_procedural_view_pk_player_reducer::insert_sender_procedural_view_pk_player; pub use insert_view_pk_membership_reducer::insert_view_pk_membership; pub use insert_view_pk_membership_secondary_reducer::insert_view_pk_membership_secondary; pub use insert_view_pk_player_reducer::insert_view_pk_player; +pub use procedural_view_pk_player_source_table::*; +pub use procedural_view_pk_player_source_type::ProceduralViewPkPlayerSource; +pub use procedural_view_pk_player_type::ProceduralViewPkPlayer; +pub use sender_procedural_view_pk_player_source_table::*; +pub use sender_procedural_view_pk_player_source_type::SenderProceduralViewPkPlayerSource; +pub use sender_procedural_view_pk_players_table::*; pub use sender_view_pk_players_a_table::*; pub use sender_view_pk_players_b_table::*; +pub use update_procedural_view_pk_player_reducer::update_procedural_view_pk_player; +pub use update_sender_procedural_view_pk_player_reducer::update_sender_procedural_view_pk_player; pub use update_view_pk_player_reducer::update_view_pk_player; pub use view_pk_membership_secondary_table::*; pub use view_pk_membership_secondary_type::ViewPkMembershipSecondary; @@ -42,9 +64,13 @@ pub use view_pk_player_type::ViewPkPlayer; /// to indicate which reducer caused the event. pub enum Reducer { + InsertProceduralViewPkPlayer { record_id: u64, id: u64, name: String }, + InsertSenderProceduralViewPkPlayer { record_id: u64, id: u64, name: String }, InsertViewPkMembership { id: u64, player_id: u64 }, InsertViewPkMembershipSecondary { id: u64, player_id: u64 }, InsertViewPkPlayer { id: u64, name: String }, + UpdateProceduralViewPkPlayer { record_id: u64, id: u64, name: String }, + UpdateSenderProceduralViewPkPlayer { record_id: u64, id: u64, name: String }, UpdateViewPkPlayer { id: u64, name: String }, } @@ -55,9 +81,13 @@ impl __sdk::InModule for Reducer { impl __sdk::Reducer for Reducer { fn reducer_name(&self) -> &'static str { match self { + Reducer::InsertProceduralViewPkPlayer { .. } => "insert_procedural_view_pk_player", + Reducer::InsertSenderProceduralViewPkPlayer { .. } => "insert_sender_procedural_view_pk_player", Reducer::InsertViewPkMembership { .. } => "insert_view_pk_membership", Reducer::InsertViewPkMembershipSecondary { .. } => "insert_view_pk_membership_secondary", Reducer::InsertViewPkPlayer { .. } => "insert_view_pk_player", + Reducer::UpdateProceduralViewPkPlayer { .. } => "update_procedural_view_pk_player", + Reducer::UpdateSenderProceduralViewPkPlayer { .. } => "update_sender_procedural_view_pk_player", Reducer::UpdateViewPkPlayer { .. } => "update_view_pk_player", _ => unreachable!(), } @@ -65,6 +95,20 @@ impl __sdk::Reducer for Reducer { #[allow(clippy::clone_on_copy)] fn args_bsatn(&self) -> Result, __sats::bsatn::EncodeError> { match self { + Reducer::InsertProceduralViewPkPlayer { record_id, id, name } => __sats::bsatn::to_vec( + &insert_procedural_view_pk_player_reducer::InsertProceduralViewPkPlayerArgs { + record_id: record_id.clone(), + id: id.clone(), + name: name.clone(), + }, + ), + Reducer::InsertSenderProceduralViewPkPlayer { record_id, id, name } => __sats::bsatn::to_vec( + &insert_sender_procedural_view_pk_player_reducer::InsertSenderProceduralViewPkPlayerArgs { + record_id: record_id.clone(), + id: id.clone(), + name: name.clone(), + }, + ), Reducer::InsertViewPkMembership { id, player_id } => { __sats::bsatn::to_vec(&insert_view_pk_membership_reducer::InsertViewPkMembershipArgs { id: id.clone(), @@ -83,6 +127,20 @@ impl __sdk::Reducer for Reducer { name: name.clone(), }) } + Reducer::UpdateProceduralViewPkPlayer { record_id, id, name } => __sats::bsatn::to_vec( + &update_procedural_view_pk_player_reducer::UpdateProceduralViewPkPlayerArgs { + record_id: record_id.clone(), + id: id.clone(), + name: name.clone(), + }, + ), + Reducer::UpdateSenderProceduralViewPkPlayer { record_id, id, name } => __sats::bsatn::to_vec( + &update_sender_procedural_view_pk_player_reducer::UpdateSenderProceduralViewPkPlayerArgs { + record_id: record_id.clone(), + id: id.clone(), + name: name.clone(), + }, + ), Reducer::UpdateViewPkPlayer { id, name } => { __sats::bsatn::to_vec(&update_view_pk_player_reducer::UpdateViewPkPlayerArgs { id: id.clone(), @@ -98,7 +156,11 @@ impl __sdk::Reducer for Reducer { #[allow(non_snake_case)] #[doc(hidden)] pub struct DbUpdate { + all_procedural_view_pk_players: __sdk::TableUpdate, all_view_pk_players: __sdk::TableUpdate, + procedural_view_pk_player_source: __sdk::TableUpdate, + sender_procedural_view_pk_player_source: __sdk::TableUpdate, + sender_procedural_view_pk_players: __sdk::TableUpdate, sender_view_pk_players_a: __sdk::TableUpdate, sender_view_pk_players_b: __sdk::TableUpdate, view_pk_membership: __sdk::TableUpdate, @@ -112,9 +174,21 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { let mut db_update = DbUpdate::default(); for table_update in __sdk::transaction_update_iter_table_updates(raw) { match &table_update.table_name[..] { + "all_procedural_view_pk_players" => db_update + .all_procedural_view_pk_players + .append(all_procedural_view_pk_players_table::parse_table_update(table_update)?), "all_view_pk_players" => db_update .all_view_pk_players .append(all_view_pk_players_table::parse_table_update(table_update)?), + "procedural_view_pk_player_source" => db_update.procedural_view_pk_player_source.append( + procedural_view_pk_player_source_table::parse_table_update(table_update)?, + ), + "sender_procedural_view_pk_player_source" => db_update.sender_procedural_view_pk_player_source.append( + sender_procedural_view_pk_player_source_table::parse_table_update(table_update)?, + ), + "sender_procedural_view_pk_players" => db_update.sender_procedural_view_pk_players.append( + sender_procedural_view_pk_players_table::parse_table_update(table_update)?, + ), "sender_view_pk_players_a" => db_update .sender_view_pk_players_a .append(sender_view_pk_players_a_table::parse_table_update(table_update)?), @@ -148,6 +222,18 @@ impl __sdk::DbUpdate for DbUpdate { fn apply_to_client_cache(&self, cache: &mut __sdk::ClientCache) -> AppliedDiff<'_> { let mut diff = AppliedDiff::default(); + diff.procedural_view_pk_player_source = cache + .apply_diff_to_table::( + "procedural_view_pk_player_source", + &self.procedural_view_pk_player_source, + ) + .with_updates_by_pk(|row| &row.record_id); + diff.sender_procedural_view_pk_player_source = cache + .apply_diff_to_table::( + "sender_procedural_view_pk_player_source", + &self.sender_procedural_view_pk_player_source, + ) + .with_updates_by_pk(|row| &row.record_id); diff.view_pk_membership = cache .apply_diff_to_table::("view_pk_membership", &self.view_pk_membership) .with_updates_by_pk(|row| &row.id); @@ -160,9 +246,21 @@ impl __sdk::DbUpdate for DbUpdate { diff.view_pk_player = cache .apply_diff_to_table::("view_pk_player", &self.view_pk_player) .with_updates_by_pk(|row| &row.id); + diff.all_procedural_view_pk_players = cache + .apply_diff_to_table::( + "all_procedural_view_pk_players", + &self.all_procedural_view_pk_players, + ) + .with_updates_by_pk(|row| &row.id); diff.all_view_pk_players = cache .apply_diff_to_table::("all_view_pk_players", &self.all_view_pk_players) .with_updates_by_pk(|row| &row.id); + diff.sender_procedural_view_pk_players = cache + .apply_diff_to_table::( + "sender_procedural_view_pk_players", + &self.sender_procedural_view_pk_players, + ) + .with_updates_by_pk(|row| &row.id); diff.sender_view_pk_players_a = cache .apply_diff_to_table::("sender_view_pk_players_a", &self.sender_view_pk_players_a) .with_updates_by_pk(|row| &row.id); @@ -176,9 +274,21 @@ impl __sdk::DbUpdate for DbUpdate { let mut db_update = DbUpdate::default(); for table_rows in raw.tables { match &table_rows.table[..] { + "all_procedural_view_pk_players" => db_update + .all_procedural_view_pk_players + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "all_view_pk_players" => db_update .all_view_pk_players .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "procedural_view_pk_player_source" => db_update + .procedural_view_pk_player_source + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "sender_procedural_view_pk_player_source" => db_update + .sender_procedural_view_pk_player_source + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "sender_procedural_view_pk_players" => db_update + .sender_procedural_view_pk_players + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "sender_view_pk_players_a" => db_update .sender_view_pk_players_a .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -205,9 +315,21 @@ impl __sdk::DbUpdate for DbUpdate { let mut db_update = DbUpdate::default(); for table_rows in raw.tables { match &table_rows.table[..] { + "all_procedural_view_pk_players" => db_update + .all_procedural_view_pk_players + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "all_view_pk_players" => db_update .all_view_pk_players .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "procedural_view_pk_player_source" => db_update + .procedural_view_pk_player_source + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "sender_procedural_view_pk_player_source" => db_update + .sender_procedural_view_pk_player_source + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "sender_procedural_view_pk_players" => db_update + .sender_procedural_view_pk_players + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "sender_view_pk_players_a" => db_update .sender_view_pk_players_a .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -236,7 +358,11 @@ impl __sdk::DbUpdate for DbUpdate { #[allow(non_snake_case)] #[doc(hidden)] pub struct AppliedDiff<'r> { + all_procedural_view_pk_players: __sdk::TableAppliedDiff<'r, ProceduralViewPkPlayer>, all_view_pk_players: __sdk::TableAppliedDiff<'r, ViewPkPlayer>, + procedural_view_pk_player_source: __sdk::TableAppliedDiff<'r, ProceduralViewPkPlayerSource>, + sender_procedural_view_pk_player_source: __sdk::TableAppliedDiff<'r, SenderProceduralViewPkPlayerSource>, + sender_procedural_view_pk_players: __sdk::TableAppliedDiff<'r, ProceduralViewPkPlayer>, sender_view_pk_players_a: __sdk::TableAppliedDiff<'r, ViewPkPlayer>, sender_view_pk_players_b: __sdk::TableAppliedDiff<'r, ViewPkPlayer>, view_pk_membership: __sdk::TableAppliedDiff<'r, ViewPkMembership>, @@ -251,7 +377,27 @@ impl __sdk::InModule for AppliedDiff<'_> { impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { fn invoke_row_callbacks(&self, event: &EventContext, callbacks: &mut __sdk::DbCallbacks) { + callbacks.invoke_table_row_callbacks::( + "all_procedural_view_pk_players", + &self.all_procedural_view_pk_players, + event, + ); callbacks.invoke_table_row_callbacks::("all_view_pk_players", &self.all_view_pk_players, event); + callbacks.invoke_table_row_callbacks::( + "procedural_view_pk_player_source", + &self.procedural_view_pk_player_source, + event, + ); + callbacks.invoke_table_row_callbacks::( + "sender_procedural_view_pk_player_source", + &self.sender_procedural_view_pk_player_source, + event, + ); + callbacks.invoke_table_row_callbacks::( + "sender_procedural_view_pk_players", + &self.sender_procedural_view_pk_players, + event, + ); callbacks.invoke_table_row_callbacks::( "sender_view_pk_players_a", &self.sender_view_pk_players_a, @@ -926,7 +1072,11 @@ impl __sdk::SpacetimeModule for RemoteModule { type QueryBuilder = __sdk::QueryBuilder; fn register_tables(client_cache: &mut __sdk::ClientCache) { + all_procedural_view_pk_players_table::register_table(client_cache); all_view_pk_players_table::register_table(client_cache); + procedural_view_pk_player_source_table::register_table(client_cache); + sender_procedural_view_pk_player_source_table::register_table(client_cache); + sender_procedural_view_pk_players_table::register_table(client_cache); sender_view_pk_players_a_table::register_table(client_cache); sender_view_pk_players_b_table::register_table(client_cache); view_pk_membership_table::register_table(client_cache); @@ -934,7 +1084,11 @@ impl __sdk::SpacetimeModule for RemoteModule { view_pk_player_table::register_table(client_cache); } const ALL_TABLE_NAMES: &'static [&'static str] = &[ + "all_procedural_view_pk_players", "all_view_pk_players", + "procedural_view_pk_player_source", + "sender_procedural_view_pk_player_source", + "sender_procedural_view_pk_players", "sender_view_pk_players_a", "sender_view_pk_players_b", "view_pk_membership", diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/procedural_view_pk_player_source_table.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/procedural_view_pk_player_source_table.rs new file mode 100644 index 00000000000..733d0e52879 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/procedural_view_pk_player_source_table.rs @@ -0,0 +1,161 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::procedural_view_pk_player_source_type::ProceduralViewPkPlayerSource; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `procedural_view_pk_player_source`. +/// +/// Obtain a handle from the [`ProceduralViewPkPlayerSourceTableAccess::procedural_view_pk_player_source`] method on [`super::RemoteTables`], +/// like `ctx.db.procedural_view_pk_player_source()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.procedural_view_pk_player_source().on_insert(...)`. +pub struct ProceduralViewPkPlayerSourceTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `procedural_view_pk_player_source`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait ProceduralViewPkPlayerSourceTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`ProceduralViewPkPlayerSourceTableHandle`], which mediates access to the table `procedural_view_pk_player_source`. + fn procedural_view_pk_player_source(&self) -> ProceduralViewPkPlayerSourceTableHandle<'_>; +} + +impl ProceduralViewPkPlayerSourceTableAccess for super::RemoteTables { + fn procedural_view_pk_player_source(&self) -> ProceduralViewPkPlayerSourceTableHandle<'_> { + ProceduralViewPkPlayerSourceTableHandle { + imp: self + .imp + .get_table::("procedural_view_pk_player_source"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct ProceduralViewPkPlayerSourceInsertCallbackId(__sdk::CallbackId); +pub struct ProceduralViewPkPlayerSourceDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for ProceduralViewPkPlayerSourceTableHandle<'ctx> { + type Row = ProceduralViewPkPlayerSource; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = ProceduralViewPkPlayerSourceInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ProceduralViewPkPlayerSourceInsertCallbackId { + ProceduralViewPkPlayerSourceInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: ProceduralViewPkPlayerSourceInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = ProceduralViewPkPlayerSourceDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ProceduralViewPkPlayerSourceDeleteCallbackId { + ProceduralViewPkPlayerSourceDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: ProceduralViewPkPlayerSourceDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct ProceduralViewPkPlayerSourceUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for ProceduralViewPkPlayerSourceTableHandle<'ctx> { + type UpdateCallbackId = ProceduralViewPkPlayerSourceUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> ProceduralViewPkPlayerSourceUpdateCallbackId { + ProceduralViewPkPlayerSourceUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: ProceduralViewPkPlayerSourceUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `record_id` unique index on the table `procedural_view_pk_player_source`, +/// which allows point queries on the field of the same name +/// via the [`ProceduralViewPkPlayerSourceRecordIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.procedural_view_pk_player_source().record_id().find(...)`. +pub struct ProceduralViewPkPlayerSourceRecordIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> ProceduralViewPkPlayerSourceTableHandle<'ctx> { + /// Get a handle on the `record_id` unique index on the table `procedural_view_pk_player_source`. + pub fn record_id(&self) -> ProceduralViewPkPlayerSourceRecordIdUnique<'ctx> { + ProceduralViewPkPlayerSourceRecordIdUnique { + imp: self.imp.get_unique_constraint::("record_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> ProceduralViewPkPlayerSourceRecordIdUnique<'ctx> { + /// Find the subscribed row whose `record_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::("procedural_view_pk_player_source"); + _table.add_unique_constraint::("record_id", |row| &row.record_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 `ProceduralViewPkPlayerSource`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait procedural_view_pk_player_sourceQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `ProceduralViewPkPlayerSource`. + fn procedural_view_pk_player_source(&self) -> __sdk::__query_builder::Table; +} + +impl procedural_view_pk_player_sourceQueryTableAccess for __sdk::QueryTableAccessor { + fn procedural_view_pk_player_source(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("procedural_view_pk_player_source") + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/procedural_view_pk_player_source_type.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/procedural_view_pk_player_source_type.rs new file mode 100644 index 00000000000..05fa2a124aa --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/procedural_view_pk_player_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 ProceduralViewPkPlayerSource { + pub record_id: u64, + pub id: u64, + pub name: String, +} + +impl __sdk::InModule for ProceduralViewPkPlayerSource { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `ProceduralViewPkPlayerSource`. +/// +/// Provides typed access to columns for query building. +pub struct ProceduralViewPkPlayerSourceCols { + pub record_id: __sdk::__query_builder::Col, + pub id: __sdk::__query_builder::Col, + pub name: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for ProceduralViewPkPlayerSource { + type Cols = ProceduralViewPkPlayerSourceCols; + fn cols(table_name: &'static str) -> Self::Cols { + ProceduralViewPkPlayerSourceCols { + record_id: __sdk::__query_builder::Col::new(table_name, "record_id"), + id: __sdk::__query_builder::Col::new(table_name, "id"), + name: __sdk::__query_builder::Col::new(table_name, "name"), + } + } +} + +/// Indexed column accessor struct for the table `ProceduralViewPkPlayerSource`. +/// +/// Provides typed access to indexed columns for query building. +pub struct ProceduralViewPkPlayerSourceIxCols { + pub id: __sdk::__query_builder::IxCol, + pub record_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for ProceduralViewPkPlayerSource { + type IxCols = ProceduralViewPkPlayerSourceIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + ProceduralViewPkPlayerSourceIxCols { + id: __sdk::__query_builder::IxCol::new(table_name, "id"), + record_id: __sdk::__query_builder::IxCol::new(table_name, "record_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for ProceduralViewPkPlayerSource {} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/procedural_view_pk_player_type.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/procedural_view_pk_player_type.rs new file mode 100644 index 00000000000..17ae4a6f9fe --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/procedural_view_pk_player_type.rs @@ -0,0 +1,34 @@ +// 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 ProceduralViewPkPlayer { + pub id: u64, + pub name: String, +} + +impl __sdk::InModule for ProceduralViewPkPlayer { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `ProceduralViewPkPlayer`. +/// +/// Provides typed access to columns for query building. +pub struct ProceduralViewPkPlayerCols { + pub id: __sdk::__query_builder::Col, + pub name: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for ProceduralViewPkPlayer { + type Cols = ProceduralViewPkPlayerCols; + fn cols(table_name: &'static str) -> Self::Cols { + ProceduralViewPkPlayerCols { + id: __sdk::__query_builder::Col::new(table_name, "id"), + name: __sdk::__query_builder::Col::new(table_name, "name"), + } + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/sender_procedural_view_pk_player_source_table.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/sender_procedural_view_pk_player_source_table.rs new file mode 100644 index 00000000000..e0d3b8c7719 --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/sender_procedural_view_pk_player_source_table.rs @@ -0,0 +1,166 @@ +// 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::sender_procedural_view_pk_player_source_type::SenderProceduralViewPkPlayerSource; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `sender_procedural_view_pk_player_source`. +/// +/// Obtain a handle from the [`SenderProceduralViewPkPlayerSourceTableAccess::sender_procedural_view_pk_player_source`] method on [`super::RemoteTables`], +/// like `ctx.db.sender_procedural_view_pk_player_source()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.sender_procedural_view_pk_player_source().on_insert(...)`. +pub struct SenderProceduralViewPkPlayerSourceTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `sender_procedural_view_pk_player_source`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait SenderProceduralViewPkPlayerSourceTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`SenderProceduralViewPkPlayerSourceTableHandle`], which mediates access to the table `sender_procedural_view_pk_player_source`. + fn sender_procedural_view_pk_player_source(&self) -> SenderProceduralViewPkPlayerSourceTableHandle<'_>; +} + +impl SenderProceduralViewPkPlayerSourceTableAccess for super::RemoteTables { + fn sender_procedural_view_pk_player_source(&self) -> SenderProceduralViewPkPlayerSourceTableHandle<'_> { + SenderProceduralViewPkPlayerSourceTableHandle { + imp: self + .imp + .get_table::("sender_procedural_view_pk_player_source"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct SenderProceduralViewPkPlayerSourceInsertCallbackId(__sdk::CallbackId); +pub struct SenderProceduralViewPkPlayerSourceDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for SenderProceduralViewPkPlayerSourceTableHandle<'ctx> { + type Row = SenderProceduralViewPkPlayerSource; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = SenderProceduralViewPkPlayerSourceInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> SenderProceduralViewPkPlayerSourceInsertCallbackId { + SenderProceduralViewPkPlayerSourceInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: SenderProceduralViewPkPlayerSourceInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = SenderProceduralViewPkPlayerSourceDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> SenderProceduralViewPkPlayerSourceDeleteCallbackId { + SenderProceduralViewPkPlayerSourceDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: SenderProceduralViewPkPlayerSourceDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct SenderProceduralViewPkPlayerSourceUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for SenderProceduralViewPkPlayerSourceTableHandle<'ctx> { + type UpdateCallbackId = SenderProceduralViewPkPlayerSourceUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> SenderProceduralViewPkPlayerSourceUpdateCallbackId { + SenderProceduralViewPkPlayerSourceUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: SenderProceduralViewPkPlayerSourceUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `record_id` unique index on the table `sender_procedural_view_pk_player_source`, +/// which allows point queries on the field of the same name +/// via the [`SenderProceduralViewPkPlayerSourceRecordIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.sender_procedural_view_pk_player_source().record_id().find(...)`. +pub struct SenderProceduralViewPkPlayerSourceRecordIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> SenderProceduralViewPkPlayerSourceTableHandle<'ctx> { + /// Get a handle on the `record_id` unique index on the table `sender_procedural_view_pk_player_source`. + pub fn record_id(&self) -> SenderProceduralViewPkPlayerSourceRecordIdUnique<'ctx> { + SenderProceduralViewPkPlayerSourceRecordIdUnique { + imp: self.imp.get_unique_constraint::("record_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> SenderProceduralViewPkPlayerSourceRecordIdUnique<'ctx> { + /// Find the subscribed row whose `record_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_procedural_view_pk_player_source"); + _table.add_unique_constraint::("record_id", |row| &row.record_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 `SenderProceduralViewPkPlayerSource`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait sender_procedural_view_pk_player_sourceQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `SenderProceduralViewPkPlayerSource`. + fn sender_procedural_view_pk_player_source( + &self, + ) -> __sdk::__query_builder::Table; +} + +impl sender_procedural_view_pk_player_sourceQueryTableAccess for __sdk::QueryTableAccessor { + fn sender_procedural_view_pk_player_source( + &self, + ) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("sender_procedural_view_pk_player_source") + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/sender_procedural_view_pk_player_source_type.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/sender_procedural_view_pk_player_source_type.rs new file mode 100644 index 00000000000..d804405c03b --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/sender_procedural_view_pk_player_source_type.rs @@ -0,0 +1,60 @@ +// 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 SenderProceduralViewPkPlayerSource { + pub record_id: u64, + pub owner: __sdk::Identity, + pub id: u64, + pub name: String, +} + +impl __sdk::InModule for SenderProceduralViewPkPlayerSource { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `SenderProceduralViewPkPlayerSource`. +/// +/// Provides typed access to columns for query building. +pub struct SenderProceduralViewPkPlayerSourceCols { + pub record_id: __sdk::__query_builder::Col, + pub owner: __sdk::__query_builder::Col, + pub id: __sdk::__query_builder::Col, + pub name: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for SenderProceduralViewPkPlayerSource { + type Cols = SenderProceduralViewPkPlayerSourceCols; + fn cols(table_name: &'static str) -> Self::Cols { + SenderProceduralViewPkPlayerSourceCols { + record_id: __sdk::__query_builder::Col::new(table_name, "record_id"), + owner: __sdk::__query_builder::Col::new(table_name, "owner"), + id: __sdk::__query_builder::Col::new(table_name, "id"), + name: __sdk::__query_builder::Col::new(table_name, "name"), + } + } +} + +/// Indexed column accessor struct for the table `SenderProceduralViewPkPlayerSource`. +/// +/// Provides typed access to indexed columns for query building. +pub struct SenderProceduralViewPkPlayerSourceIxCols { + pub owner: __sdk::__query_builder::IxCol, + pub record_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for SenderProceduralViewPkPlayerSource { + type IxCols = SenderProceduralViewPkPlayerSourceIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + SenderProceduralViewPkPlayerSourceIxCols { + owner: __sdk::__query_builder::IxCol::new(table_name, "owner"), + record_id: __sdk::__query_builder::IxCol::new(table_name, "record_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for SenderProceduralViewPkPlayerSource {} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/sender_procedural_view_pk_players_table.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/sender_procedural_view_pk_players_table.rs new file mode 100644 index 00000000000..703f609a2fd --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/sender_procedural_view_pk_players_table.rs @@ -0,0 +1,161 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::procedural_view_pk_player_type::ProceduralViewPkPlayer; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `sender_procedural_view_pk_players`. +/// +/// Obtain a handle from the [`SenderProceduralViewPkPlayersTableAccess::sender_procedural_view_pk_players`] method on [`super::RemoteTables`], +/// like `ctx.db.sender_procedural_view_pk_players()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.sender_procedural_view_pk_players().on_insert(...)`. +pub struct SenderProceduralViewPkPlayersTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `sender_procedural_view_pk_players`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait SenderProceduralViewPkPlayersTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`SenderProceduralViewPkPlayersTableHandle`], which mediates access to the table `sender_procedural_view_pk_players`. + fn sender_procedural_view_pk_players(&self) -> SenderProceduralViewPkPlayersTableHandle<'_>; +} + +impl SenderProceduralViewPkPlayersTableAccess for super::RemoteTables { + fn sender_procedural_view_pk_players(&self) -> SenderProceduralViewPkPlayersTableHandle<'_> { + SenderProceduralViewPkPlayersTableHandle { + imp: self + .imp + .get_table::("sender_procedural_view_pk_players"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct SenderProceduralViewPkPlayersInsertCallbackId(__sdk::CallbackId); +pub struct SenderProceduralViewPkPlayersDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for SenderProceduralViewPkPlayersTableHandle<'ctx> { + type Row = ProceduralViewPkPlayer; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = SenderProceduralViewPkPlayersInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> SenderProceduralViewPkPlayersInsertCallbackId { + SenderProceduralViewPkPlayersInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: SenderProceduralViewPkPlayersInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = SenderProceduralViewPkPlayersDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> SenderProceduralViewPkPlayersDeleteCallbackId { + SenderProceduralViewPkPlayersDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: SenderProceduralViewPkPlayersDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct SenderProceduralViewPkPlayersUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for SenderProceduralViewPkPlayersTableHandle<'ctx> { + type UpdateCallbackId = SenderProceduralViewPkPlayersUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> SenderProceduralViewPkPlayersUpdateCallbackId { + SenderProceduralViewPkPlayersUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: SenderProceduralViewPkPlayersUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `id` unique index on the table `sender_procedural_view_pk_players`, +/// which allows point queries on the field of the same name +/// via the [`SenderProceduralViewPkPlayersIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.sender_procedural_view_pk_players().id().find(...)`. +pub struct SenderProceduralViewPkPlayersIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> SenderProceduralViewPkPlayersTableHandle<'ctx> { + /// Get a handle on the `id` unique index on the table `sender_procedural_view_pk_players`. + pub fn id(&self) -> SenderProceduralViewPkPlayersIdUnique<'ctx> { + SenderProceduralViewPkPlayersIdUnique { + imp: self.imp.get_unique_constraint::("id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> SenderProceduralViewPkPlayersIdUnique<'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_procedural_view_pk_players"); + _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 `ProceduralViewPkPlayer`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait sender_procedural_view_pk_playersQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `ProceduralViewPkPlayer`. + fn sender_procedural_view_pk_players(&self) -> __sdk::__query_builder::Table; +} + +impl sender_procedural_view_pk_playersQueryTableAccess for __sdk::QueryTableAccessor { + fn sender_procedural_view_pk_players(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("sender_procedural_view_pk_players") + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/update_procedural_view_pk_player_reducer.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/update_procedural_view_pk_player_reducer.rs new file mode 100644 index 00000000000..6500cd0000e --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/update_procedural_view_pk_player_reducer.rs @@ -0,0 +1,76 @@ +// 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 UpdateProceduralViewPkPlayerArgs { + pub record_id: u64, + pub id: u64, + pub name: String, +} + +impl From for super::Reducer { + fn from(args: UpdateProceduralViewPkPlayerArgs) -> Self { + Self::UpdateProceduralViewPkPlayer { + record_id: args.record_id, + id: args.id, + name: args.name, + } + } +} + +impl __sdk::InModule for UpdateProceduralViewPkPlayerArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `update_procedural_view_pk_player`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait update_procedural_view_pk_player { + /// Request that the remote module invoke the reducer `update_procedural_view_pk_player` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and this method provides no way to listen for its completion status. + /// /// Use [`update_procedural_view_pk_player:update_procedural_view_pk_player_then`] to run a callback after the reducer completes. + fn update_procedural_view_pk_player(&self, record_id: u64, id: u64, name: String) -> __sdk::Result<()> { + self.update_procedural_view_pk_player_then(record_id, id, name, |_, _| {}) + } + + /// Request that the remote module invoke the reducer `update_procedural_view_pk_player` to run as soon as possible, + /// registering `callback` to run when we are notified that the reducer completed. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed with the `callback`. + fn update_procedural_view_pk_player_then( + &self, + record_id: u64, + id: u64, + name: String, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()>; +} + +impl update_procedural_view_pk_player for super::RemoteReducers { + fn update_procedural_view_pk_player_then( + &self, + record_id: u64, + id: u64, + name: String, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()> { + self.imp + .invoke_reducer_with_callback(UpdateProceduralViewPkPlayerArgs { record_id, id, name }, callback) + } +} diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/update_sender_procedural_view_pk_player_reducer.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/update_sender_procedural_view_pk_player_reducer.rs new file mode 100644 index 00000000000..c20d64a97dd --- /dev/null +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/update_sender_procedural_view_pk_player_reducer.rs @@ -0,0 +1,76 @@ +// 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 UpdateSenderProceduralViewPkPlayerArgs { + pub record_id: u64, + pub id: u64, + pub name: String, +} + +impl From for super::Reducer { + fn from(args: UpdateSenderProceduralViewPkPlayerArgs) -> Self { + Self::UpdateSenderProceduralViewPkPlayer { + record_id: args.record_id, + id: args.id, + name: args.name, + } + } +} + +impl __sdk::InModule for UpdateSenderProceduralViewPkPlayerArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `update_sender_procedural_view_pk_player`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait update_sender_procedural_view_pk_player { + /// Request that the remote module invoke the reducer `update_sender_procedural_view_pk_player` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and this method provides no way to listen for its completion status. + /// /// Use [`update_sender_procedural_view_pk_player:update_sender_procedural_view_pk_player_then`] to run a callback after the reducer completes. + fn update_sender_procedural_view_pk_player(&self, record_id: u64, id: u64, name: String) -> __sdk::Result<()> { + self.update_sender_procedural_view_pk_player_then(record_id, id, name, |_, _| {}) + } + + /// Request that the remote module invoke the reducer `update_sender_procedural_view_pk_player` to run as soon as possible, + /// registering `callback` to run when we are notified that the reducer completed. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed with the `callback`. + fn update_sender_procedural_view_pk_player_then( + &self, + record_id: u64, + id: u64, + name: String, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()>; +} + +impl update_sender_procedural_view_pk_player for super::RemoteReducers { + fn update_sender_procedural_view_pk_player_then( + &self, + record_id: u64, + id: u64, + name: String, + + callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + + Send + + 'static, + ) -> __sdk::Result<()> { + self.imp + .invoke_reducer_with_callback(UpdateSenderProceduralViewPkPlayerArgs { record_id, id, name }, callback) + } +} 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..8154f53ed00 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) @@ -116,6 +125,124 @@ async fn exec_view_pk_on_update(db_name: &str) { test_counter.wait_for_all().await; } +/// Subscribe to a procedural view with an explicitly declared primary key. +/// Ensures the rust sdk exposes `on_update` for the view and receives old/new rows for the same key. +async fn exec_procedural_view_pk_on_update(db_name: &str) { + let test_counter = TestCounter::new(); + let mut on_update = Some(test_counter.add_test("procedural_view_on_update")); + + connect_then(db_name, &test_counter, move |ctx| { + subscribe_these_then(ctx, &["SELECT * FROM all_procedural_view_pk_players"], move |ctx| { + ctx.db + .all_procedural_view_pk_players() + .on_update(move |_, old_row, new_row| { + assert_eq!(old_row.id, 1); + assert_eq!(old_row.name, "before"); + assert_eq!(new_row.id, 1); + assert_eq!(new_row.name, "after"); + put_result(&mut on_update, Ok(())); + }); + + ctx.reducers() + .insert_procedural_view_pk_player_then( + 1, + 1, + "before".to_string(), + reducer_callback_assert_committed("insert_procedural_view_pk_player"), + ) + .unwrap(); + + ctx.reducers() + .update_procedural_view_pk_player_then( + 1, + 1, + "after".to_string(), + reducer_callback_assert_committed("update_procedural_view_pk_player"), + ) + .unwrap(); + }); + }) + .await; + + test_counter.wait_for_all().await; +} + +/// Subscribe two different senders to a sender-scoped procedural view whose primary key values overlap. +/// Ensures each sender only receives updates for rows visible to that sender, even when the view PK value is the same. +async fn exec_sender_procedural_view_pk_updates_are_scoped(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")); + + let _sender_a = connect_then(db_name, &test_counter, move |ctx| { + subscribe_these_then(ctx, &["SELECT * FROM sender_procedural_view_pk_players"], move |ctx| { + ctx.db + .sender_procedural_view_pk_players() + .on_update(move |_, old_row, new_row| { + assert_eq!(old_row.id, 7); + assert_eq!(old_row.name, "sender-a-before"); + assert_eq!(new_row.id, 7); + assert_eq!(new_row.name, "sender-a-after"); + put_result(&mut sender_a_update, Ok(())); + }); + + ctx.reducers() + .insert_sender_procedural_view_pk_player_then( + 100, + 7, + "sender-a-before".to_string(), + reducer_callback_assert_committed("insert_sender_procedural_view_pk_player"), + ) + .unwrap(); + + ctx.reducers() + .update_sender_procedural_view_pk_player_then( + 100, + 7, + "sender-a-after".to_string(), + reducer_callback_assert_committed("update_sender_procedural_view_pk_player"), + ) + .unwrap(); + }); + }) + .await; + + let _sender_b = connect_then_named(db_name, &test_counter, "sender_b_on_connect", move |ctx| { + subscribe_these_then(ctx, &["SELECT * FROM sender_procedural_view_pk_players"], move |ctx| { + ctx.db + .sender_procedural_view_pk_players() + .on_update(move |_, old_row, new_row| { + assert_eq!(old_row.id, 7); + assert_eq!(old_row.name, "sender-b-before"); + assert_eq!(new_row.id, 7); + assert_eq!(new_row.name, "sender-b-after"); + put_result(&mut sender_b_update, Ok(())); + }); + + ctx.reducers() + .insert_sender_procedural_view_pk_player_then( + 200, + 7, + "sender-b-before".to_string(), + reducer_callback_assert_committed("insert_sender_procedural_view_pk_player"), + ) + .unwrap(); + + ctx.reducers() + .update_sender_procedural_view_pk_player_then( + 200, + 7, + "sender-b-after".to_string(), + reducer_callback_assert_committed("update_sender_procedural_view_pk_player"), + ) + .unwrap(); + }); + }) + .await; + + test_counter.wait_for_all().await; +} + /// Subscribe to a right semijoin whose rhs is a view with primary key. /// /// Ensures: @@ -285,6 +412,10 @@ async fn exec_view_pk_semijoin_two_sender_views_query_builder(db_name: &str) { pub async fn dispatch(test: &str, db_name: &str) { match test { "view-pk-on-update" => exec_view_pk_on_update(db_name).await, + "procedural-view-pk-on-update" => exec_procedural_view_pk_on_update(db_name).await, + "sender-procedural-view-pk-updates-are-scoped" => { + exec_sender_procedural_view_pk_updates_are_scoped(db_name).await + } "view-pk-join-query-builder" => exec_view_pk_join_query_builder(db_name).await, "view-pk-semijoin-two-sender-views-query-builder" => { exec_view_pk_semijoin_two_sender_views_query_builder(db_name).await