From 4bfb4c5ab307536545eaa1184d6618c291cd7810 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 8 Oct 2025 13:03:42 -0700 Subject: [PATCH 1/8] Add macro bindings for views --- crates/bindings-macro/src/lib.rs | 9 + crates/bindings-macro/src/reducer.rs | 5 +- crates/bindings-macro/src/table.rs | 2 +- crates/bindings-macro/src/view.rs | 178 +++++++++++ crates/bindings/src/lib.rs | 52 ++++ crates/bindings/src/rt.rs | 372 ++++++++++++++++++++++- crates/bindings/tests/ui/reducers.stderr | 16 +- crates/bindings/tests/ui/views.rs | 70 ++++- crates/bindings/tests/ui/views.stderr | 364 ++++++++++++++++++++++ 9 files changed, 1040 insertions(+), 28 deletions(-) create mode 100644 crates/bindings-macro/src/view.rs diff --git a/crates/bindings-macro/src/lib.rs b/crates/bindings-macro/src/lib.rs index cd85b80ef65..e5ca3ad52c7 100644 --- a/crates/bindings-macro/src/lib.rs +++ b/crates/bindings-macro/src/lib.rs @@ -12,6 +12,7 @@ mod reducer; mod sats; mod table; mod util; +mod view; use proc_macro::TokenStream as StdTokenStream; use proc_macro2::TokenStream; @@ -112,6 +113,14 @@ pub fn reducer(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream { }) } +#[proc_macro_attribute] +pub fn view(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream { + cvt_attr::(args, item, quote!(), |args, original_function| { + let args = view::ViewArgs::parse(args)?; + view::view_impl(args, original_function) + }) +} + /// It turns out to be shockingly difficult to construct an [`Attribute`]. /// That type is not [`Parse`], instead having two distinct methods /// for parsing "inner" vs "outer" attributes. diff --git a/crates/bindings-macro/src/reducer.rs b/crates/bindings-macro/src/reducer.rs index ff98cc6250b..2ca10c48b4f 100644 --- a/crates/bindings-macro/src/reducer.rs +++ b/crates/bindings-macro/src/reducer.rs @@ -139,11 +139,12 @@ pub(crate) fn reducer_impl(args: ReducerArgs, original_function: &ItemFn) -> syn } } #[automatically_derived] - impl spacetimedb::rt::ReducerInfo for #func_name { + impl spacetimedb::rt::FnInfo for #func_name { + type Invoke = spacetimedb::rt::ReducerFn; const NAME: &'static str = #reducer_name; #(const LIFECYCLE: Option = Some(#lifecycle);)* const ARG_NAMES: &'static [Option<&'static str>] = &[#(#opt_arg_names),*]; - const INVOKE: spacetimedb::rt::ReducerFn = #func_name::invoke; + const INVOKE: Self::Invoke = #func_name::invoke; } }) } diff --git a/crates/bindings-macro/src/table.rs b/crates/bindings-macro/src/table.rs index 4e2b079992f..dd67ec22e9d 100644 --- a/crates/bindings-macro/src/table.rs +++ b/crates/bindings-macro/src/table.rs @@ -821,7 +821,7 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R let reducer = &sched.reducer; let scheduled_at_id = scheduled_at_column.index; let desc = quote!(spacetimedb::table::ScheduleDesc { - reducer_name: <#reducer as spacetimedb::rt::ReducerInfo>::NAME, + reducer_name: <#reducer as spacetimedb::rt::FnInfo>::NAME, scheduled_at_column: #scheduled_at_id, }); diff --git a/crates/bindings-macro/src/view.rs b/crates/bindings-macro/src/view.rs new file mode 100644 index 00000000000..ba6a529ebe3 --- /dev/null +++ b/crates/bindings-macro/src/view.rs @@ -0,0 +1,178 @@ +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::parse::Parser; +use syn::{FnArg, ItemFn}; + +use crate::sym; +use crate::util::{ident_to_litstr, match_meta}; + +pub(crate) struct ViewArgs { + #[allow(unused)] + public: bool, +} + +impl ViewArgs { + /// Parse `#[view(public)]` where `public` is required. + pub(crate) fn parse(input: TokenStream) -> syn::Result { + if input.is_empty() { + return Err(syn::Error::new( + Span::call_site(), + "views must be declared as `#[view(public)]`; `public` is required", + )); + } + let mut public = false; + syn::meta::parser(|meta| { + match_meta!(match meta { + sym::public => { + if public { + return Err(syn::Error::new( + Span::call_site(), + "duplicate attribute argument: `public`", + )); + } + public = true; + } + }); + Ok(()) + }) + .parse2(input)?; + if !public { + return Err(syn::Error::new( + Span::call_site(), + "views must be declared as `#[view(public)]`; `public` is required", + )); + } + Ok(Self { public }) + } +} + +pub(crate) fn view_impl(_args: ViewArgs, original_function: &ItemFn) -> syn::Result { + let func_name = &original_function.sig.ident; + let view_name = ident_to_litstr(func_name); + let vis = &original_function.vis; + + for param in &original_function.sig.generics.params { + let err = |msg| syn::Error::new_spanned(param, msg); + match param { + syn::GenericParam::Lifetime(_) => {} + syn::GenericParam::Type(_) => return Err(err("type parameters are not allowed on views")), + syn::GenericParam::Const(_) => return Err(err("const parameters are not allowed on views")), + } + } + + // Extract parameters + let typed_args = original_function + .sig + .inputs + .iter() + .map(|arg| match arg { + FnArg::Typed(arg) => Ok(arg), + FnArg::Receiver(_) => Err(syn::Error::new_spanned( + arg, + "The `self` parameter is not allowed in views", + )), + }) + .collect::>>()?; + + // Extract parameter names + let opt_arg_names = typed_args.iter().map(|arg| { + if let syn::Pat::Ident(i) = &*arg.pat { + let name = i.ident.to_string(); + quote!(Some(#name)) + } else { + quote!(None) + } + }); + + let arg_tys = typed_args.iter().map(|arg| arg.ty.as_ref()).collect::>(); + + // Extract the context type and the rest of the parameter types + let [ctx_ty, arg_tys @ ..] = &arg_tys[..] else { + return Err(syn::Error::new_spanned( + &original_function.sig, + "Views must always have a context parameter: `&ViewContext` or `&AnonymousViewContext`", + )); + }; + + // Extract the context type + let ctx_ty = match ctx_ty { + syn::Type::Reference(ctx_ty) => ctx_ty.elem.as_ref(), + _ => { + return Err(syn::Error::new_spanned( + ctx_ty, + "The first parameter of a view must be a context parameter: `&ViewContext` or `&AnonymousViewContext`; passed by reference", + )); + } + }; + + // Views must return a result + let ret_ty = match &original_function.sig.output { + syn::ReturnType::Type(_, t) => t.as_ref(), + syn::ReturnType::Default => { + return Err(syn::Error::new_spanned( + &original_function.sig, + "views must return `Vec` or `Option` where `T` is a `SpacetimeType`", + )); + } + }; + + let register_describer_symbol = format!("__preinit__20_register_describer_{}", view_name.value()); + + let lt_params = &original_function.sig.generics; + let lt_where_clause = <_params.where_clause; + + let generated_describe_function = quote! { + #[export_name = #register_describer_symbol] + pub extern "C" fn __register_describer() { + spacetimedb::rt::ViewRegistrar::<#ctx_ty>::register::<_, #func_name, _, _>(#func_name) + } + }; + + Ok(quote! { + const _: () = { #generated_describe_function }; + + #[allow(non_camel_case_types)] + #vis struct #func_name { _never: ::core::convert::Infallible } + + const _: () = { + fn _assert_args #lt_params () #lt_where_clause { + let _ = <#ctx_ty as spacetimedb::rt::ViewContextArg>::_ITEM; + let _ = <#ret_ty as spacetimedb::rt::ViewReturn>::_ITEM; + } + }; + + const _: () = { + fn _assert_args #lt_params () #lt_where_clause { + #(let _ = <#arg_tys as spacetimedb::rt::ViewArg>::_ITEM;)* + } + }; + + impl #func_name { + fn invoke(__ctx: #ctx_ty, __args: &[u8]) -> Vec { + spacetimedb::rt::ViewDispatcher::<#ctx_ty>::invoke::<_, _, _>(#func_name, __ctx, __args) + } + } + + #[automatically_derived] + impl spacetimedb::rt::FnInfo for #func_name { + /// The type of this function + type Invoke = as spacetimedb::rt::ViewKindTrait>::InvokeFn; + + /// The name of this function + const NAME: &'static str = #view_name; + + /// The parameter names of this function + const ARG_NAMES: &'static [Option<&'static str>] = &[#(#opt_arg_names),*]; + + /// The pointer for invoking this function + const INVOKE: Self::Invoke = #func_name::invoke; + + /// The return type of this function + fn return_type( + ts: &mut impl spacetimedb::sats::typespace::TypespaceBuilder + ) -> Option { + Some(<#ret_ty as spacetimedb::SpacetimeType>::make_type(ts)) + } + } + }) +} diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index a0b18970f27..1edd585270a 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -667,6 +667,58 @@ pub use spacetimedb_bindings_macro::table; #[doc(inline)] pub use spacetimedb_bindings_macro::reducer; +/// Marks a function as a spacetimedb view. +/// +/// A view is a function with read-only access to the database. +/// +/// The first argument of a view is always a [`&ViewContext`] or [`&AnonymousViewContext`]. +/// The former can only read from the database whereas latter can also access info about the caller. +/// +/// After this, a view can take any number of arguments just like reducers. +/// These arguments must implement the [`SpacetimeType`], [`Serialize`], and [`Deserialize`] traits. +/// All of these traits can be derived at once by marking a type with `#[derive(SpacetimeType)]`. +/// +/// Views return `Vec` or `Option` where `T` is a `SpacetimeType`. +/// +/// ```no_run +/// # mod demo { +/// use spacetimedb::{view, table, AnonymousViewContext, ViewContext}; +/// use spacetimedb_lib::Identity; +/// +/// #[table(name = player)] +/// struct Player { +/// #[unique] +/// identity: Identity, +/// #[index(btree)] +/// level: u32, +/// } +/// +/// #[view(public)] +/// pub fn my_player(ctx: &ViewContext) -> Option { +/// ctx.db.player().identity().find(ctx.sender) +/// } +/// +/// #[view(public)] +/// pub fn players_at_level(ctx: &AnonymousViewContext, level: u32) -> Vec { +/// ctx.db.player().level().filter(level).collect() +/// } +/// # } +/// ``` +/// +/// Just like reducers, views are limited in their ability to interact with the outside world. +/// They have no access to any network or filesystem interfaces. +/// Calling methods from [`std::io`], [`std::net`], or [`std::fs`] will result in runtime errors. +/// +/// Views are callable by reducers and other views simply by passing their `ViewContext`.. +/// This is a regular function call. +/// The callee will run within the caller's transaction. +/// +/// +/// [`&ViewContext`]: `ViewContext` +/// [`&AnonymousViewContext`]: `AnonymousViewContext` +#[doc(inline)] +pub use spacetimedb_bindings_macro::view; + /// One of two possible types that can be passed as the first argument to a `#[view]`. /// The other is [`ViewContext`]. /// Use this type if the view does not depend on the caller's identity. diff --git a/crates/bindings/src/rt.rs b/crates/bindings/src/rt.rs index be513ea52f4..739dda3635d 100644 --- a/crates/bindings/src/rt.rs +++ b/crates/bindings/src/rt.rs @@ -1,20 +1,38 @@ #![deny(unsafe_op_in_unsafe_fn)] use crate::table::IndexAlgo; -use crate::{sys, IterBuf, ReducerContext, ReducerResult, SpacetimeType, Table}; +use crate::{ + sys, AnonymousViewContext, IterBuf, LocalReadOnly, ReducerContext, ReducerResult, SpacetimeType, Table, ViewContext, +}; pub use spacetimedb_lib::db::raw_def::v9::Lifecycle as LifecycleReducer; use spacetimedb_lib::db::raw_def::v9::{RawIndexAlgorithm, RawModuleDefV9Builder, TableType}; use spacetimedb_lib::de::{self, Deserialize, Error as _, SeqProductAccess}; use spacetimedb_lib::sats::typespace::TypespaceBuilder; use spacetimedb_lib::sats::{impl_deserialize, impl_serialize, ProductTypeElement}; use spacetimedb_lib::ser::{Serialize, SerializeSeqProduct}; -use spacetimedb_lib::{bsatn, ConnectionId, Identity, ProductType, RawModuleDef, Timestamp}; +use spacetimedb_lib::{bsatn, AlgebraicType, ConnectionId, Identity, ProductType, RawModuleDef, Timestamp}; use spacetimedb_primitives::*; use std::fmt; use std::marker::PhantomData; use std::sync::{Mutex, OnceLock}; use sys::raw::{BytesSink, BytesSource}; +pub trait IntoVec { + fn into_vec(self) -> Vec; +} + +impl IntoVec for Vec { + fn into_vec(self) -> Vec { + self + } +} + +impl IntoVec for Option { + fn into_vec(self) -> Vec { + self.into_iter().collect() + } +} + /// The `sender` invokes `reducer` at `timestamp` and provides it with the given `args`. /// /// Returns an invalid buffer on success @@ -43,19 +61,84 @@ pub trait Reducer<'de, A: Args<'de>> { fn invoke(&self, ctx: &ReducerContext, args: A) -> ReducerResult; } -/// A trait for types that can *describe* a reducer. -pub trait ReducerInfo { - /// The name of the reducer. +/// Invoke a caller-specific view. +/// Returns a BSATN encoded `Vec` of rows. +pub fn invoke_view<'a, A: Args<'a>, T: SpacetimeType + Serialize>( + view: impl View<'a, A, T>, + ctx: ViewContext, + args: &'a [u8], +) -> Vec { + // Deserialize the arguments from a bsatn encoding. + let SerDeArgs(args) = bsatn::from_slice(args).expect("unable to decode args"); + let rows: Vec = view.invoke(&ctx, args); + let mut buf = IterBuf::take(); + buf.serialize_into(&rows).expect("unable to encode rows"); + std::mem::take(&mut *buf) +} +/// A trait for types representing the execution logic of a caller-specific view. +#[diagnostic::on_unimplemented( + message = "invalid view signature", + label = "this view signature is not valid", + note = "", + note = "view signatures must match:", + note = " `Fn(&ViewContext, [T1, ...]) -> Vec | Option`", + note = "where each `Ti` implements `SpacetimeType`.", + note = "" +)] +pub trait View<'de, A: Args<'de>, T: SpacetimeType + Serialize> { + fn invoke(&self, ctx: &ViewContext, args: A) -> Vec; +} + +/// Invoke an anonymous view. +/// Returns a BSATN encoded `Vec` of rows. +pub fn invoke_anonymous_view<'a, A: Args<'a>, T: SpacetimeType + Serialize>( + view: impl AnonymousView<'a, A, T>, + ctx: AnonymousViewContext, + args: &'a [u8], +) -> Vec { + // Deserialize the arguments from a bsatn encoding. + let SerDeArgs(args) = bsatn::from_slice(args).expect("unable to decode args"); + let rows: Vec = view.invoke(&ctx, args); + let mut buf = IterBuf::take(); + buf.serialize_into(&rows).expect("unable to encode rows"); + std::mem::take(&mut *buf) +} +/// A trait for types representing the execution logic of an anonymous view. +#[diagnostic::on_unimplemented( + message = "invalid anonymous view signature", + label = "this view signature is not valid", + note = "", + note = "anonymous view signatures must match:", + note = " `Fn(&AnonymousViewContext, [T1, ...]) -> Vec | Option`", + note = "where each `Ti` implements `SpacetimeType`.", + note = "" +)] +pub trait AnonymousView<'de, A: Args<'de>, T: SpacetimeType + Serialize> { + fn invoke(&self, ctx: &AnonymousViewContext, args: A) -> Vec; +} + +/// A trait for types that can *describe* a callable function such as a reducer or view. +pub trait FnInfo { + /// The type of function to invoke. + type Invoke; + + /// The name of the function. const NAME: &'static str; - /// The lifecycle of the reducer, if there is one. + /// The lifecycle of the function, if there is one. const LIFECYCLE: Option = None; - /// A description of the parameter names of the reducer. + /// A description of the parameter names of the function. const ARG_NAMES: &'static [Option<&'static str>]; - /// The function to call to invoke the reducer. - const INVOKE: ReducerFn; + /// The function to invoke. + const INVOKE: Self::Invoke; + + /// The return type of this function. + /// Currently only implemented for views. + fn return_type(_ts: &mut impl TypespaceBuilder) -> Option { + None + } } /// A trait of types representing the arguments of a reducer. @@ -69,8 +152,8 @@ pub trait Args<'de>: Sized { /// Serialize the arguments in `self` into the sequence `prod` according to the type `S`. fn serialize_seq_product(&self, prod: &mut S) -> Result<(), S::Error>; - /// Returns the schema for this reducer provided a `typespace`. - fn schema(typespace: &mut impl TypespaceBuilder) -> ProductType; + /// Returns the schema of the args for this function provided a `typespace`. + fn schema(typespace: &mut impl TypespaceBuilder) -> ProductType; } /// A trait of types representing the result of executing a reducer. @@ -119,6 +202,113 @@ pub trait ReducerArg { } impl ReducerArg for T {} +#[diagnostic::on_unimplemented( + message = "The first parameter of a `#[view]` must be `&ViewContext` or `&AnonymousViewContext`" +)] +pub trait ViewContextArg { + #[doc(hidden)] + const _ITEM: () = (); +} +impl ViewContextArg for ViewContext {} +impl ViewContextArg for AnonymousViewContext {} + +/// A trait of types that can be an argument of a view. +#[diagnostic::on_unimplemented( + message = "the view argument `{Self}` does not implement `SpacetimeType`", + note = "if you own the type, try adding `#[derive(SpacetimeType)]` to its definition" +)] +pub trait ViewArg { + #[doc(hidden)] + const _ITEM: () = (); +} +impl ViewArg for T {} + +/// A trait of types that can be the return type of a view. +#[diagnostic::on_unimplemented(message = "Views must return `Vec` or `Option` where `T` is a `SpacetimeType`")] +pub trait ViewReturn { + #[doc(hidden)] + const _ITEM: () = (); +} +impl ViewReturn for Vec {} +impl ViewReturn for Option {} + +/// Map the correct dispatcher based on the `Ctx` type +pub struct ViewKind { + _marker: PhantomData, +} + +pub trait ViewKindTrait { + type InvokeFn; +} + +impl ViewKindTrait for ViewKind { + type InvokeFn = ViewFn; +} + +impl ViewKindTrait for ViewKind { + type InvokeFn = AnonymousFn; +} + +/// Invoke the correct dispatcher based on the `Ctx` type +pub struct ViewDispatcher { + _marker: PhantomData, +} + +impl ViewDispatcher { + #[inline] + pub fn invoke<'a, A, T, V>(view: V, ctx: ViewContext, args: &'a [u8]) -> Vec + where + A: Args<'a>, + T: SpacetimeType + Serialize, + V: View<'a, A, T>, + { + invoke_view(view, ctx, args) + } +} + +impl ViewDispatcher { + #[inline] + pub fn invoke<'a, A, T, V>(view: V, ctx: AnonymousViewContext, args: &'a [u8]) -> Vec + where + A: Args<'a>, + T: SpacetimeType + Serialize, + V: AnonymousView<'a, A, T>, + { + invoke_anonymous_view(view, ctx, args) + } +} + +/// Register the correct dispatcher based on the `Ctx` type +pub struct ViewRegistrar { + _marker: PhantomData, +} + +impl ViewRegistrar { + #[inline] + pub fn register<'a, A, I, T, V>(view: V) + where + A: Args<'a>, + T: SpacetimeType + Serialize, + I: FnInfo, + V: View<'a, A, T>, + { + register_view::(view) + } +} + +impl ViewRegistrar { + #[inline] + pub fn register<'a, A, I, T, V>(view: V) + where + A: Args<'a>, + T: SpacetimeType + Serialize, + I: FnInfo, + V: AnonymousView<'a, A, T>, + { + register_anonymous_view::(view) + } +} + /// Assert that a reducer type-checks with a given type. pub const fn scheduled_reducer_typecheck<'de, Row>(_x: impl ReducerForScheduledTable<'de, Row>) where @@ -239,7 +429,7 @@ macro_rules! impl_reducer { #[inline] #[allow(non_snake_case, irrefutable_let_patterns)] - fn schema(_typespace: &mut impl TypespaceBuilder) -> ProductType { + fn schema(_typespace: &mut impl TypespaceBuilder) -> ProductType { // Extract the names of the arguments. let [.., $($T),*] = Info::ARG_NAMES else { panic!() }; ProductType::new(vec![ @@ -264,6 +454,37 @@ macro_rules! impl_reducer { } } + // Implement `View<..., ViewContext>` for the tuple type `($($T,)*)`. + impl<'de, Func, Elem, Retn, $($T),*> + View<'de, ($($T,)*), Elem> for Func + where + $($T: SpacetimeType + Deserialize<'de> + Serialize,)* + Func: Fn(&ViewContext, $($T),*) -> Retn, + Retn: IntoVec, + Elem: SpacetimeType + Serialize, + { + #[allow(non_snake_case)] + fn invoke(&self, ctx: &ViewContext, args: ($($T,)*)) -> Vec { + let ($($T,)*) = args; + self(ctx, $($T),*).into_vec() + } + } + + // Implement `View<..., AnonymousViewContext>` for the tuple type `($($T,)*)`. + impl<'de, Func, Elem, Retn, $($T),*> + AnonymousView<'de, ($($T,)*), Elem> for Func + where + $($T: SpacetimeType + Deserialize<'de> + Serialize,)* + Func: Fn(&AnonymousViewContext, $($T),*) -> Retn, + Retn: IntoVec, + Elem: SpacetimeType + Serialize, + { + #[allow(non_snake_case)] + fn invoke(&self, ctx: &AnonymousViewContext, args: ($($T,)*)) -> Vec { + let ($($T,)*) = args; + self(ctx, $($T),*).into_vec() + } + } }; // Counts the number of elements in the tuple. (@count $($T:ident)*) => { @@ -362,7 +583,7 @@ impl From> for RawIndexAlgorithm { } /// Registers a describer for the reducer `I` with arguments `A`. -pub fn register_reducer<'a, A: Args<'a>, I: ReducerInfo>(_: impl Reducer<'a, A>) { +pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { register_describer(|module| { let params = A::schema::(&mut module.inner); module.inner.add_reducer(I::NAME, params, I::LIFECYCLE); @@ -370,6 +591,36 @@ pub fn register_reducer<'a, A: Args<'a>, I: ReducerInfo>(_: impl Reducer<'a, A>) }) } +/// Registers a describer for the view `I` with arguments `A` and return type `Vec`. +pub fn register_view<'a, A, I, T>(_: impl View<'a, A, T>) +where + A: Args<'a>, + I: FnInfo, + T: SpacetimeType + Serialize, +{ + register_describer(|module| { + let params = A::schema::(&mut module.inner); + let return_type = I::return_type(&mut module.inner).unwrap(); + module.inner.add_view(I::NAME, true, false, params, return_type); + module.views.push(I::INVOKE); + }) +} + +/// Registers a describer for the anonymous view `I` with arguments `A` and return type `Vec`. +pub fn register_anonymous_view<'a, A, I, T>(_: impl AnonymousView<'a, A, T>) +where + A: Args<'a>, + I: FnInfo, + T: SpacetimeType + Serialize, +{ + register_describer(|module| { + let params = A::schema::(&mut module.inner); + let return_type = I::return_type(&mut module.inner).unwrap(); + module.inner.add_view(I::NAME, true, true, params, return_type); + module.views_anon.push(I::INVOKE); + }) +} + /// Registers a row-level security policy. pub fn register_row_level_security(sql: &'static str) { register_describer(|module| { @@ -379,11 +630,15 @@ pub fn register_row_level_security(sql: &'static str) { /// A builder for a module. #[derive(Default)] -struct ModuleBuilder { +pub struct ModuleBuilder { /// The module definition. inner: RawModuleDefV9Builder, /// The reducers of the module. reducers: Vec, + /// The client specific views of the module. + views: Vec, + /// The anonymous views of the module. + views_anon: Vec, } // Not actually a mutex; because WASM is single-threaded this basically just turns into a refcell. @@ -394,6 +649,14 @@ static DESCRIBERS: Mutex>> = Mutex::new(Vec::new()); pub type ReducerFn = fn(ReducerContext, &[u8]) -> ReducerResult; static REDUCERS: OnceLock> = OnceLock::new(); +/// A view function takes in `(ViewContext, Args)` and returns a Vec of bytes. +pub type ViewFn = fn(ViewContext, &[u8]) -> Vec; +static VIEWS: OnceLock> = OnceLock::new(); + +/// An anonymous view function takes in `(AnonymousViewContext, Args)` and returns a Vec of bytes. +pub type AnonymousFn = fn(AnonymousViewContext, &[u8]) -> Vec; +static ANONYMOUS_VIEWS: OnceLock> = OnceLock::new(); + /// Called by the host when the module is initialized /// to describe the module into a serialized form that is returned. /// @@ -422,8 +685,10 @@ extern "C" fn __describe_module__(description: BytesSink) { let module_def = RawModuleDef::V9(module_def); let bytes = bsatn::to_vec(&module_def).expect("unable to serialize typespace"); - // Write the set of reducers. + // Write the set of reducers and views. REDUCERS.set(module.reducers).ok().unwrap(); + VIEWS.set(module.views).ok().unwrap(); + ANONYMOUS_VIEWS.set(module.views_anon).ok().unwrap(); // Write the bsatn data into the sink. write_to_sink(description, &bytes); @@ -504,6 +769,81 @@ extern "C" fn __call_reducer__( } } +/// Called by the host to execute an anonymous view. +/// +/// The `args` is a `BytesSource`, registered on the host side, +/// which can be read with `bytes_source_read`. +/// The contents of the buffer are the BSATN-encoding of the arguments to the view. +/// In the case of empty arguments, `args` will be 0, that is, invalid. +/// +/// The output of the view is written to a `BytesSink`, +/// registered on the host side, with `bytes_sink_write`. +#[no_mangle] +extern "C" fn __call_view_anon__(id: usize, args: BytesSource, sink: BytesSink) -> i16 { + let views = ANONYMOUS_VIEWS.get().unwrap(); + write_to_sink( + sink, + &with_read_args(args, |args| { + views[id](AnonymousViewContext { db: LocalReadOnly {} }, args) + }), + ); + 0 +} + +/// Called by the host to execute a view when the `sender` calls the view identified by `id` with `args`. +/// See [`__call_reducer__`] for more commentary on the arguments. +/// +/// The `args` is a `BytesSource`, registered on the host side, +/// which can be read with `bytes_source_read`. +/// The contents of the buffer are the BSATN-encoding of the arguments to the view. +/// In the case of empty arguments, `args` will be 0, that is, invalid. +/// +/// The output of the view is written to a `BytesSink`, +/// registered on the host side, with `bytes_sink_write`. +#[no_mangle] +extern "C" fn __call_view__( + id: usize, + sender_0: u64, + sender_1: u64, + sender_2: u64, + sender_3: u64, + conn_id_0: u64, + conn_id_1: u64, + args: BytesSource, + sink: BytesSink, +) -> i16 { + // Piece together `sender_i` into an `Identity`. + let sender = [sender_0, sender_1, sender_2, sender_3]; + let sender: [u8; 32] = bytemuck::must_cast(sender); + let sender = Identity::from_byte_array(sender); // The LITTLE-ENDIAN constructor. + + // Piece together `conn_id_i` into a `ConnectionId`. + // The all-zeros `ConnectionId` (`ConnectionId::ZERO`) is interpreted as `None`. + let conn_id = [conn_id_0, conn_id_1]; + let conn_id: [u8; 16] = bytemuck::must_cast(conn_id); + let conn_id = ConnectionId::from_le_byte_array(conn_id); // The LITTLE-ENDIAN constructor. + let conn_id = (conn_id != ConnectionId::ZERO).then_some(conn_id); + + let views = VIEWS.get().unwrap(); + let db = LocalReadOnly {}; + let connection_id = conn_id; + + write_to_sink( + sink, + &with_read_args(args, |args| { + views[id]( + ViewContext { + sender, + connection_id, + db, + }, + args, + ) + }), + ); + 0 +} + /// Run `logic` with `args` read from the host into a `&[u8]`. fn with_read_args(args: BytesSource, logic: impl FnOnce(&[u8]) -> R) -> R { if args == BytesSource::INVALID { @@ -624,7 +964,7 @@ macro_rules! __make_register_reftype { #[cfg(feature = "unstable")] #[doc(hidden)] -pub fn volatile_nonatomic_schedule_immediate<'de, A: Args<'de>, R: Reducer<'de, A>, R2: ReducerInfo>( +pub fn volatile_nonatomic_schedule_immediate<'de, A: Args<'de>, R: Reducer<'de, A>, R2: FnInfo>( _reducer: R, args: A, ) { diff --git a/crates/bindings/tests/ui/reducers.stderr b/crates/bindings/tests/ui/reducers.stderr index 14814c9e055..91775aa9ee2 100644 --- a/crates/bindings/tests/ui/reducers.stderr +++ b/crates/bindings/tests/ui/reducers.stderr @@ -37,8 +37,8 @@ error[E0277]: invalid reducer signature note: required by a bound in `register_reducer` --> src/rt.rs | - | pub fn register_reducer<'a, A: Args<'a>, I: ReducerInfo>(_: impl Reducer<'a, A>) { - | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` + | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { + | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` error[E0277]: the reducer argument `Test` does not implement `SpacetimeType` --> tests/ui/reducers.rs:6:40 @@ -98,8 +98,8 @@ error[E0277]: invalid reducer signature note: required by a bound in `register_reducer` --> src/rt.rs | - | pub fn register_reducer<'a, A: Args<'a>, I: ReducerInfo>(_: impl Reducer<'a, A>) { - | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` + | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { + | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` error[E0277]: `Test` is not a valid reducer return type --> tests/ui/reducers.rs:9:46 @@ -151,8 +151,8 @@ error[E0277]: invalid reducer signature note: required by a bound in `register_reducer` --> src/rt.rs | - | pub fn register_reducer<'a, A: Args<'a>, I: ReducerInfo>(_: impl Reducer<'a, A>) { - | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` + | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { + | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` error[E0277]: the first argument of a reducer must be `&ReducerContext` --> tests/ui/reducers.rs:23:20 @@ -202,8 +202,8 @@ error[E0277]: invalid reducer signature note: required by a bound in `register_reducer` --> src/rt.rs | - | pub fn register_reducer<'a, A: Args<'a>, I: ReducerInfo>(_: impl Reducer<'a, A>) { - | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` + | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { + | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` error[E0277]: the first argument of a reducer must be `&ReducerContext` --> tests/ui/reducers.rs:26:21 diff --git a/crates/bindings/tests/ui/views.rs b/crates/bindings/tests/ui/views.rs index 131a0cf2f47..130af3f9579 100644 --- a/crates/bindings/tests/ui/views.rs +++ b/crates/bindings/tests/ui/views.rs @@ -1,4 +1,4 @@ -use spacetimedb::{reducer, table, ReducerContext}; +use spacetimedb::{reducer, table, view, AnonymousViewContext, Identity, ReducerContext, ViewContext}; #[table(name = test)] struct Test { @@ -64,4 +64,72 @@ fn read_only_btree_index_no_delete(ctx: &ReducerContext) { read_only.db.test().x().delete(0u32..); } +#[table(name = player)] +struct Player { + #[unique] + identity: Identity, +} + +struct NotSpacetimeType {} + +/// Private views not allowed; must be `#[view(public)]` +#[view] +fn view_def_no_public(_: &ViewContext) -> Vec { + vec![] +} + +/// Duplicate `public` +#[view(public, public)] +fn view_def_duplicate_attribute_arg() -> Vec { + vec![] +} + +/// Unsupported attribute arg +#[view(public, anonymous)] +fn view_def_unsupported_attribute_arg() -> Vec { + vec![] +} + +/// A `ViewContext` is required +#[view(public)] +fn view_def_no_context() -> Vec { + vec![] +} + +/// A `ViewContext` is required +#[view(public)] +fn view_def_wrong_context(_: &ReducerContext) -> Vec { + vec![] +} + +/// Must pass the `ViewContext` by ref +#[view(public)] +fn view_def_pass_context_by_value(_: ViewContext) -> Vec { + vec![] +} + +/// The view context must be the first parameter +#[view(public)] +fn view_def_wrong_context_position(_: &u32, _: &ViewContext) -> Vec { + vec![] +} + +/// Must return `Vec` or `Option` where `T` is a SpacetimeType +#[view(public)] +fn view_def_no_return(_: &ViewContext) {} + +/// Must return `Vec` or `Option` where `T` is a SpacetimeType +#[view(public)] +fn view_def_wrong_return(_: &ViewContext) -> Player { + Player { + identity: Identity::ZERO, + } +} + +/// Must return `Vec` or `Option` where `T` is a SpacetimeType +#[view(public)] +fn view_def_returns_not_a_spacetime_type(_: &AnonymousViewContext) -> Option { + None +} + fn main() {} diff --git a/crates/bindings/tests/ui/views.stderr b/crates/bindings/tests/ui/views.stderr index 95108add08c..ab8b20a2eef 100644 --- a/crates/bindings/tests/ui/views.stderr +++ b/crates/bindings/tests/ui/views.stderr @@ -1,3 +1,81 @@ +error: views must be declared as `#[view(public)]`; `public` is required + --> tests/ui/views.rs:76:1 + | +76 | #[view] + | ^^^^^^^ + | + = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: duplicate attribute argument: `public` + --> tests/ui/views.rs:82:1 + | +82 | #[view(public, public)] + | ^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: expected `public` + --> tests/ui/views.rs:88:16 + | +88 | #[view(public, anonymous)] + | ^^^^^^^^^ + +error: Views must always have a context parameter: `&ViewContext` or `&AnonymousViewContext` + --> tests/ui/views.rs:95:1 + | +95 | 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:107:38 + | +107 | fn view_def_pass_context_by_value(_: ViewContext) -> Vec { + | ^^^^^^^^^^^ + +error: views must return `Vec` or `Option` where `T` is a `SpacetimeType` + --> tests/ui/views.rs:119:1 + | +119 | fn view_def_no_return(_: &ViewContext) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error[E0277]: the trait bound `ViewKind: ViewKindTrait` is not satisfied + --> tests/ui/views.rs:100:1 + | +100 | #[view(public)] + | ^^^^^^^^^^^^^^^ the trait `ViewKindTrait` is not implemented for `ViewKind` + | + = help: the following other types implement trait `ViewKindTrait`: + ViewKind + 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:100:1 + | +100 | #[view(public)] + | ^^^^^^^^^^^^^^^ impl has extra requirement `ViewKind: ViewKindTrait` + | + = 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 `ViewKind: ViewKindTrait` is not satisfied + --> tests/ui/views.rs:112:1 + | +112 | #[view(public)] + | ^^^^^^^^^^^^^^^ the trait `ViewKindTrait` is not implemented for `ViewKind` + | + = help: the following other types implement trait `ViewKindTrait`: + ViewKind + 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:112:1 + | +112 | #[view(public)] + | ^^^^^^^^^^^^^^^ impl has extra requirement `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 | @@ -88,3 +166,289 @@ error[E0599]: no method named `delete` found for struct `RangedIndexReadOnly` in | 64 | 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:100:1 + | +100 | #[view(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:101:31 + | +101 | fn view_def_wrong_context(_: &ReducerContext) -> Vec { + | ^^^^^^^^^^^^^^ the trait `ViewContextArg` is not implemented for `ReducerContext` + | + = help: the following other types implement trait `ViewContextArg`: + AnonymousViewContext + ViewContext + +error[E0599]: no function or associated item named `invoke` found for struct `ViewDispatcher` in the current scope + --> tests/ui/views.rs:100:1 + | +100 | #[view(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[E0599]: no function or associated item named `register` found for struct `ViewRegistrar` in the current scope + --> tests/ui/views.rs:112:1 + | +112 | #[view(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:113:40 + | +113 | fn view_def_wrong_context_position(_: &u32, _: &ViewContext) -> Vec { + | ^^^ the trait `ViewContextArg` is not implemented for `u32` + | + = help: the following other types implement trait `ViewContextArg`: + AnonymousViewContext + ViewContext + +error[E0277]: the view argument `&ViewContext` does not implement `SpacetimeType` + --> tests/ui/views.rs:113:48 + | +113 | fn view_def_wrong_context_position(_: &u32, _: &ViewContext) -> Vec { + | ^^^^^^^^^^^^ the trait `SpacetimeType` is not implemented for `ViewContext` + | + = note: if you own the type, try adding `#[derive(SpacetimeType)]` to its definition + = help: the following other types implement trait `SpacetimeType`: + &T + () + AlgebraicType + AlgebraicTypeRef + Arc + ArrayType + Box + ColId + and $N others + = note: required for `&ViewContext` to implement `SpacetimeType` + = note: required for `&ViewContext` to implement `ViewArg` + +error[E0599]: no function or associated item named `invoke` found for struct `ViewDispatcher` in the current scope + --> tests/ui/views.rs:112:1 + | +112 | #[view(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:122:1 + | +122 | #[view(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}` + = note: + = note: view signatures must match: + = note: `Fn(&ViewContext, [T1, ...]) -> Vec | Option` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `ViewRegistrar::::register` + --> src/rt.rs + | + | pub fn register<'a, A, I, T, V>(view: V) + | -------- required by a bound in this associated function +... + | V: View<'a, A, T>, + | ^^^^^^^^^^^^^^ required by this 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:123:46 + | +123 | fn view_def_wrong_return(_: &ViewContext) -> Player { + | ^^^^^^ the trait `ViewReturn` is not implemented for `Player` + | + = help: the following other types implement trait `ViewReturn`: + Option + Vec + +error[E0277]: invalid view signature + --> tests/ui/views.rs:122:1 + | +122 | #[view(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}` + = note: + = note: view signatures must match: + = note: `Fn(&ViewContext, [T1, ...]) -> Vec | Option` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `ViewDispatcher::::invoke` + --> src/rt.rs + | + | pub fn invoke<'a, A, T, V>(view: V, ctx: ViewContext, args: &'a [u8]) -> Vec + | ------ required by a bound in this associated function +... + | V: View<'a, A, T>, + | ^^^^^^^^^^^^^^ required by this 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:1 + | +130 | #[view(public)] + | ^^^^^^^^^^^^^^^ the trait `SpacetimeType` is not implemented for `NotSpacetimeType` + | + = note: if you own the type, try adding `#[derive(SpacetimeType)]` to its definition + = help: the following other types implement trait `SpacetimeType`: + &T + () + AlgebraicType + AlgebraicTypeRef + Arc + ArrayType + Box + ColId + and $N others + = note: required for `for<'a> fn(&'a AnonymousViewContext) -> Option {view_def_returns_not_a_spacetime_type}` to implement `AnonymousView<'_, (), NotSpacetimeType>` +note: required by a bound in `ViewRegistrar::::register` + --> src/rt.rs + | + | pub fn register<'a, A, I, T, V>(view: V) + | -------- required by a bound in this associated function +... + | V: AnonymousView<'a, A, T>, + | ^^^^^^^^^^^^^^^^^^^^^^^ required by this 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: Serialize` is not satisfied + --> tests/ui/views.rs:130:1 + | +130 | #[view(public)] + | ^^^^^^^^^^^^^^^ the trait `Serialize` is not implemented for `NotSpacetimeType` + | + = help: the following other types implement trait `Serialize`: + &T + () + (T0, T1) + (T0, T1, T2) + (T0, T1, T2, T3) + (T0, T1, T2, T3, T4) + (T0, T1, T2, T3, T4, T5) + (T0, T1, T2, T3, T4, T5, T6) + and $N others + = note: required for `for<'a> fn(&'a AnonymousViewContext) -> Option {view_def_returns_not_a_spacetime_type}` to implement `AnonymousView<'_, (), NotSpacetimeType>` +note: required by a bound in `ViewRegistrar::::register` + --> src/rt.rs + | + | pub fn register<'a, A, I, T, V>(view: V) + | -------- required by a bound in this associated function +... + | V: AnonymousView<'a, A, T>, + | ^^^^^^^^^^^^^^^^^^^^^^^ required by this 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:131:71 + | +131 | fn view_def_returns_not_a_spacetime_type(_: &AnonymousViewContext) -> Option { + | ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `SpacetimeType` is not implemented for `NotSpacetimeType` + | + = note: if you own the type, try adding `#[derive(SpacetimeType)]` to its definition + = help: the following other types implement trait `SpacetimeType`: + &T + () + AlgebraicType + AlgebraicTypeRef + Arc + ArrayType + Box + ColId + and $N others + = note: required for `Option` to implement `ViewReturn` + +error[E0277]: the trait bound `NotSpacetimeType: SpacetimeType` is not satisfied + --> tests/ui/views.rs:130:1 + | +130 | #[view(public)] + | ^^^^^^^^^^^^^^^ the trait `SpacetimeType` is not implemented for `NotSpacetimeType` + | + = note: if you own the type, try adding `#[derive(SpacetimeType)]` to its definition + = help: the following other types implement trait `SpacetimeType`: + &T + () + AlgebraicType + AlgebraicTypeRef + Arc + ArrayType + Box + ColId + and $N others + = note: required for `for<'a> fn(&'a AnonymousViewContext) -> Option {view_def_returns_not_a_spacetime_type}` to implement `AnonymousView<'_, (), NotSpacetimeType>` +note: required by a bound in `ViewDispatcher::::invoke` + --> src/rt.rs + | + | pub fn invoke<'a, A, T, V>(view: V, ctx: AnonymousViewContext, args: &'a [u8]) -> Vec + | ------ required by a bound in this associated function +... + | V: AnonymousView<'a, A, T>, + | ^^^^^^^^^^^^^^^^^^^^^^^ required by this 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: Serialize` is not satisfied + --> tests/ui/views.rs:130:1 + | +130 | #[view(public)] + | ^^^^^^^^^^^^^^^ the trait `Serialize` is not implemented for `NotSpacetimeType` + | + = help: the following other types implement trait `Serialize`: + &T + () + (T0, T1) + (T0, T1, T2) + (T0, T1, T2, T3) + (T0, T1, T2, T3, T4) + (T0, T1, T2, T3, T4, T5) + (T0, T1, T2, T3, T4, T5, T6) + and $N others + = note: required for `for<'a> fn(&'a AnonymousViewContext) -> Option {view_def_returns_not_a_spacetime_type}` to implement `AnonymousView<'_, (), NotSpacetimeType>` +note: required by a bound in `ViewDispatcher::::invoke` + --> src/rt.rs + | + | pub fn invoke<'a, A, T, V>(view: V, ctx: AnonymousViewContext, args: &'a [u8]) -> Vec + | ------ required by a bound in this associated function +... + | V: AnonymousView<'a, A, T>, + | ^^^^^^^^^^^^^^^^^^^^^^^ required by this 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:131:71 + | +131 | fn view_def_returns_not_a_spacetime_type(_: &AnonymousViewContext) -> Option { + | ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `SpacetimeType` is not implemented for `NotSpacetimeType` + | + = note: if you own the type, try adding `#[derive(SpacetimeType)]` to its definition + = help: the following other types implement trait `SpacetimeType`: + &T + () + AlgebraicType + AlgebraicTypeRef + Arc + ArrayType + Box + ColId + and $N others + = note: required for `Option` to implement `SpacetimeType` From b52a38afc0ea30fe73fd68b5cc548866b6c83976 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Tue, 21 Oct 2025 15:30:09 -0700 Subject: [PATCH 2/8] add smoketests for st_view_* tables --- .../src/locking_tx_datastore/mut_tx.rs | 4 +- smoketests/tests/views.py | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 smoketests/tests/views.py diff --git a/crates/datastore/src/locking_tx_datastore/mut_tx.rs b/crates/datastore/src/locking_tx_datastore/mut_tx.rs index b90bc053a32..11080dce79e 100644 --- a/crates/datastore/src/locking_tx_datastore/mut_tx.rs +++ b/crates/datastore/src/locking_tx_datastore/mut_tx.rs @@ -10,7 +10,7 @@ use super::{ }; use crate::system_tables::{ system_tables, ConnectionIdViaU128, StConnectionCredentialsFields, StConnectionCredentialsRow, StViewFields, - StViewParamRow, ST_CONNECTION_CREDENTIALS_ID, ST_VIEW_COLUMN_ID, ST_VIEW_ID, + StViewParamRow, ST_CONNECTION_CREDENTIALS_ID, ST_VIEW_COLUMN_ID, ST_VIEW_ID, ST_VIEW_PARAM_ID, }; use crate::traits::{InsertFlags, RowTypeForTable, TxData, UpdateFlags}; use crate::{ @@ -349,7 +349,7 @@ impl MutTxId { fn insert_into_st_view_param(&mut self, view_id: ViewId, params: &ProductType) -> Result<()> { for (i, field) in params.elements.iter().enumerate() { self.insert_via_serialize_bsatn( - ST_VIEW_COLUMN_ID, + ST_VIEW_PARAM_ID, &StViewParamRow { view_id, param_pos: i.into(), diff --git a/smoketests/tests/views.py b/smoketests/tests/views.py new file mode 100644 index 00000000000..4d07d46fbc8 --- /dev/null +++ b/smoketests/tests/views.py @@ -0,0 +1,50 @@ +from .. import Smoketest + + +class Views(Smoketest): + MODULE_CODE = """ +use spacetimedb::ViewContext; + +#[derive(Copy, Clone)] +#[spacetimedb::table(name = player_state)] +pub struct PlayerState { + #[primary_key] + id: u64, + #[index(btree)] + level: u64, +} + +#[spacetimedb::view(public)] +pub fn player(ctx: &ViewContext, id: u64) -> Option { + ctx.db.player_state().id().find(id) +} +""" + + def assertSql(self, sql, expected): + self.maxDiff = None + sql_out = self.spacetime("sql", self.database_identity, sql) + sql_out = "\n".join([line.rstrip() for line in sql_out.splitlines()]) + expected = "\n".join([line.rstrip() for line in expected.splitlines()]) + self.assertMultiLineEqual(sql_out, expected) + + def test_st_view_tables(self): + """This test asserts that views populate the st_view_* system tables""" + + self.assertSql("SELECT * FROM st_view", """\ + view_id | view_name | table_id | is_public | is_anonymous +---------+-----------+---------------+-----------+-------------- + 4096 | "player" | (some = 4097) | true | false +""") + + self.assertSql("SELECT * FROM st_view_param", """\ + view_id | param_pos | param_name | param_type +---------+-----------+------------+------------ + 4096 | 0 | "id" | 0x0d +""") + + self.assertSql("SELECT * FROM st_view_column", """\ + view_id | col_pos | col_name | col_type +---------+---------+----------+---------- + 4096 | 0 | "id" | 0x0d + 4096 | 1 | "level" | 0x0d +""") From 86170a9f16e13b3814a449c5c2aa11b0676a1173 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 22 Oct 2025 08:31:57 -0700 Subject: [PATCH 3/8] more tests --- crates/schema/src/def/validate/v9.rs | 29 ++++++++++++---------------- smoketests/tests/views.py | 29 +++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index 678d81f6ff5..b8d92cfd79c 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -465,26 +465,21 @@ impl ModuleValidator<'_> { }) }; - // The possible return types of a view are `T`, `Option`, or `Vec`, + // The possible return types of a view are `Vec` or `Option`, // where `T` is a `ProductType` in the `Typespace`. // Here we extract the inner product type ref `T`. // We exit early for errors since this breaks all the other checks. - let product_type_ref = if return_type.is_option() { - return_type - .as_option() - .and_then(AlgebraicType::as_ref) - .cloned() - .ok_or_else(invalid_return_type)? - } else if return_type.is_array() { - return_type - .as_array() - .map(|array_type| array_type.elem_ty.as_ref()) - .and_then(AlgebraicType::as_ref) - .cloned() - .ok_or_else(invalid_return_type)? - } else { - return_type.as_ref().cloned().ok_or_else(invalid_return_type)? - }; + let product_type_ref = return_type + .as_option() + .and_then(AlgebraicType::as_ref) + .or_else(|| { + return_type + .as_array() + .map(|array_type| array_type.elem_ty.as_ref()) + .and_then(AlgebraicType::as_ref) + }) + .cloned() + .ok_or_else(invalid_return_type)?; let product_type = self .typespace diff --git a/smoketests/tests/views.py b/smoketests/tests/views.py index 4d07d46fbc8..1d638159be5 100644 --- a/smoketests/tests/views.py +++ b/smoketests/tests/views.py @@ -1,4 +1,4 @@ -from .. import Smoketest +from .. import Smoketest, random_string class Views(Smoketest): @@ -48,3 +48,30 @@ def test_st_view_tables(self): 4096 | 0 | "id" | 0x0d 4096 | 1 | "level" | 0x0d """) + +class FailPublish(Smoketest): + AUTOPUBLISH = False + + MODULE_CODE_BROKEN = """ +use spacetimedb::ViewContext; + +#[spacetimedb::table(name = person, public)] +pub struct Person { + name: String, +} + +#[spacetimedb::view(public)] +pub fn person(ctx: &ViewContext) -> Option { + None +} +""" + + def test_fail_publish(self): + """This tests server side view validation on publish""" + + name = random_string() + + self.write_module_code(self.MODULE_CODE_BROKEN) + + with self.assertRaises(Exception): + self.publish_module(name) From e51cdd06e2e191ec7fe1678a83a3a60eec8e0b54 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 22 Oct 2025 09:18:25 -0700 Subject: [PATCH 4/8] more docs --- crates/bindings/src/lib.rs | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index 1edd585270a..80104be3034 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -682,26 +682,60 @@ pub use spacetimedb_bindings_macro::reducer; /// /// ```no_run /// # mod demo { -/// use spacetimedb::{view, table, AnonymousViewContext, ViewContext}; +/// use spacetimedb::{view, table, AnonymousViewContext, SpacetimeType, ViewContext}; /// use spacetimedb_lib::Identity; /// /// #[table(name = player)] /// struct Player { +/// #[auto_inc] +/// #[primary_key] +/// id: u64, +/// /// #[unique] /// identity: Identity, +/// /// #[index(btree)] /// level: u32, /// } /// +/// #[derive(SpacetimeType)] +/// struct PlayerId { +/// id: u64, +/// } +/// +/// #[table(name = location, index(name = coordinates, btree(columns = [x, y])))] +/// struct Location { +/// #[unique] +/// player_id: u64, +/// x: u64, +/// y: u64, +/// } +/// /// #[view(public)] /// pub fn my_player(ctx: &ViewContext) -> Option { /// ctx.db.player().identity().find(ctx.sender) /// } /// /// #[view(public)] +/// pub fn my_player_id(ctx: &ViewContext) -> Option { +/// ctx.db.player().identity().find(ctx.sender).map(|Player { id, .. }| PlayerId { id }) +/// } +/// +/// #[view(public)] /// pub fn players_at_level(ctx: &AnonymousViewContext, level: u32) -> Vec { /// ctx.db.player().level().filter(level).collect() /// } +/// +/// #[view(public)] +/// pub fn players_at_coordinates(ctx: &AnonymousViewContext, x: u64, y: u64) -> Vec { +/// ctx +/// .db +/// .location() +/// .coordinates() +/// .filter((x, y)) +/// .filter_map(|location| ctx.db.player().id().find(location.player_id)) +/// .collect() +/// } /// # } /// ``` /// From c174413474de7b6d80b6b5bb0bdf859b5c8989eb Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 22 Oct 2025 09:18:35 -0700 Subject: [PATCH 5/8] more tests --- smoketests/tests/views.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/smoketests/tests/views.py b/smoketests/tests/views.py index 1d638159be5..d5a2ffd163a 100644 --- a/smoketests/tests/views.py +++ b/smoketests/tests/views.py @@ -52,7 +52,7 @@ def test_st_view_tables(self): class FailPublish(Smoketest): AUTOPUBLISH = False - MODULE_CODE_BROKEN = """ + MODULE_CODE_BROKEN_NAMESPACE = """ use spacetimedb::ViewContext; #[spacetimedb::table(name = person, public)] @@ -66,12 +66,38 @@ class FailPublish(Smoketest): } """ - def test_fail_publish(self): - """This tests server side view validation on publish""" + MODULE_CODE_BROKEN_RETURN_TYPE = """ +use spacetimedb::{SpacetimeType, ViewContext}; + +#[derive(SpacetimeType)] +pub enum ABC { + A, + B, + C, +} + +#[spacetimedb::view(public)] +pub fn person(ctx: &ViewContext) -> Option { + None +} +""" + + def test_fail_publish_namespace_collision(self): + """Publishing a module should fail if a table and view have the same name""" + + name = random_string() + + self.write_module_code(self.MODULE_CODE_BROKEN_NAMESPACE) + + with self.assertRaises(Exception): + self.publish_module(name) + + def test_fail_publish_wrong_return_type(self): + """Publishing a module should fail if the inner return type is not a product type""" name = random_string() - self.write_module_code(self.MODULE_CODE_BROKEN) + self.write_module_code(self.MODULE_CODE_BROKEN_RETURN_TYPE) with self.assertRaises(Exception): self.publish_module(name) From 9ad787392913521db5111bc622522cf4ddb91f25 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 22 Oct 2025 10:30:29 -0700 Subject: [PATCH 6/8] compile test for scheduled functions --- crates/bindings/tests/ui/views.rs | 17 +++++++++++++++++ crates/bindings/tests/ui/views.stderr | 26 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/crates/bindings/tests/ui/views.rs b/crates/bindings/tests/ui/views.rs index 130af3f9579..5e5bdb09d94 100644 --- a/crates/bindings/tests/ui/views.rs +++ b/crates/bindings/tests/ui/views.rs @@ -132,4 +132,21 @@ fn view_def_returns_not_a_spacetime_type(_: &AnonymousViewContext) -> Option Vec { + vec![] +} + fn main() {} diff --git a/crates/bindings/tests/ui/views.stderr b/crates/bindings/tests/ui/views.stderr index ab8b20a2eef..99616d3c899 100644 --- a/crates/bindings/tests/ui/views.stderr +++ b/crates/bindings/tests/ui/views.stderr @@ -452,3 +452,29 @@ error[E0277]: the trait bound `NotSpacetimeType: SpacetimeType` is not satisfied ColId and $N others = note: required for `Option` to implement `SpacetimeType` + +error[E0631]: type mismatch in function arguments + --> tests/ui/views.rs:136:56 + | +136 | #[spacetimedb::table(name = scheduled_table, scheduled(scheduled_table_view))] + | -------------------------------------------------------^^^^^^^^^^^^^^^^^^^^--- + | | | + | | expected due to this + | required by a bound introduced by this call +... +148 | fn scheduled_table_view(_: &ViewContext, _args: ScheduledTable) -> Vec { + | ------------------------------------------------------------------------------ found signature defined here + | + = note: expected function signature `for<'a> fn(&'a ReducerContext, ScheduledTable) -> _` + found function signature `fn(&ViewContext, ScheduledTable) -> _` + = note: required for `for<'a> fn(&'a ViewContext, ScheduledTable) -> Vec {scheduled_table_view}` to implement `Reducer<'_, (ScheduledTable,)>` + = note: required for `for<'a> fn(&'a ViewContext, ScheduledTable) -> Vec {scheduled_table_view}` to implement `ReducerForScheduledTable<'_, ScheduledTable>` +note: required by a bound in `scheduled_reducer_typecheck` + --> src/rt.rs + | + | pub const fn scheduled_reducer_typecheck<'de, Row>(_x: impl ReducerForScheduledTable<'de, Row>) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `scheduled_reducer_typecheck` +help: consider wrapping the function in a closure + | +136 | #[spacetimedb::table(name = scheduled_table, scheduled(|arg0: &ReducerContext, arg1: ScheduledTable| scheduled_table_view(/* &ViewContext */, arg1)))] + | +++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++ From 2ec207dbae2d50bbc6d75f82416d07fd2bb14ee7 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 22 Oct 2025 11:18:56 -0700 Subject: [PATCH 7/8] update doc comments --- crates/bindings/src/lib.rs | 49 ++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index 80104be3034..0454da71a5d 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -698,6 +698,17 @@ pub use spacetimedb_bindings_macro::reducer; /// level: u32, /// } /// +/// impl Player { +/// fn merge(self, location: Location) -> PlayerAndLocation { +/// PlayerAndLocation { +/// player_id: self.id, +/// level: self.level, +/// x: location.x, +/// y: location.y, +/// } +/// } +/// } +/// /// #[derive(SpacetimeType)] /// struct PlayerId { /// id: u64, @@ -711,23 +722,35 @@ pub use spacetimedb_bindings_macro::reducer; /// y: u64, /// } /// +/// #[derive(SpacetimeType)] +/// struct PlayerAndLocation { +/// player_id: u64, +/// level: u32, +/// x: u64, +/// y: u64, +/// } +/// +/// // A view that selects at most one row from a table /// #[view(public)] -/// pub fn my_player(ctx: &ViewContext) -> Option { +/// fn my_player(ctx: &ViewContext) -> Option { /// ctx.db.player().identity().find(ctx.sender) /// } /// +/// // An example of column projection /// #[view(public)] -/// pub fn my_player_id(ctx: &ViewContext) -> Option { +/// fn my_player_id(ctx: &ViewContext) -> Option { /// ctx.db.player().identity().find(ctx.sender).map(|Player { id, .. }| PlayerId { id }) /// } /// +/// // An example of a parameterized view /// #[view(public)] -/// pub fn players_at_level(ctx: &AnonymousViewContext, level: u32) -> Vec { +/// fn players_at_level(ctx: &AnonymousViewContext, level: u32) -> Vec { /// ctx.db.player().level().filter(level).collect() /// } /// +/// // An example that is analogous to a semijoin in sql /// #[view(public)] -/// pub fn players_at_coordinates(ctx: &AnonymousViewContext, x: u64, y: u64) -> Vec { +/// fn players_at_coordinates(ctx: &AnonymousViewContext, x: u64, y: u64) -> Vec { /// ctx /// .db /// .location() @@ -736,6 +759,24 @@ pub use spacetimedb_bindings_macro::reducer; /// .filter_map(|location| ctx.db.player().id().find(location.player_id)) /// .collect() /// } +/// +/// // An example of a join that combines fields from two different tables +/// #[view(public)] +/// fn players_with_coordinates(ctx: &AnonymousViewContext, x: u64, y: u64) -> Vec { +/// ctx +/// .db +/// .location() +/// .coordinates() +/// .filter((x, y)) +/// .filter_map(|location| ctx +/// .db +/// .player() +/// .id() +/// .find(location.player_id) +/// .map(|player| player.merge(location)) +/// ) +/// .collect() +/// } /// # } /// ``` /// From 0a79c520aabb0d2d0c9c122c1efd59717ee82462 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 22 Oct 2025 12:50:01 -0700 Subject: [PATCH 8/8] fix doctest --- crates/bindings/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index 0454da71a5d..dfd0af179b8 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -762,7 +762,7 @@ pub use spacetimedb_bindings_macro::reducer; /// /// // An example of a join that combines fields from two different tables /// #[view(public)] -/// fn players_with_coordinates(ctx: &AnonymousViewContext, x: u64, y: u64) -> Vec { +/// fn players_with_coordinates(ctx: &AnonymousViewContext, x: u64, y: u64) -> Vec { /// ctx /// .db /// .location()