diff --git a/crates/bindings-macro/src/lib.rs b/crates/bindings-macro/src/lib.rs index cd85b80ef65..97c11392563 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; @@ -38,6 +39,7 @@ mod sym { } symbol!(at); + symbol!(anonymous); symbol!(auto_inc); symbol!(btree); symbol!(client_connected); @@ -112,6 +114,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..3feed09c408 --- /dev/null +++ b/crates/bindings-macro/src/view.rs @@ -0,0 +1,284 @@ +use proc_macro2::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 { + anonymous: 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( + proc_macro2::Span::call_site(), + "views must be declared as #[view(public)]; public is required", + )); + } + let mut public = false; + let mut anonymous = false; + syn::meta::parser(|meta| { + match_meta!(match meta { + sym::public => { + public = true; + } + sym::anonymous => { + anonymous = true; + } + }); + Ok(()) + }) + .parse2(input)?; + if !public { + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + "views must be declared as #[view(public)]; public is required", + )); + } + Ok(Self { anonymous }) + } +} + +fn view_impl_anon(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 all function parameters, except for `self` ones that aren't allowed. + 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, "`self` arguments not allowed in views")), + }) + .collect::>>()?; + + // Extract all function 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 + let ctx_ty = arg_tys.first().ok_or_else(|| { + syn::Error::new_spanned( + original_function.sig.fn_token, + "An anonymous view must have `&AnonymousViewContext` as its first argument", + ) + })?; + + // Extract the return type + let ret_ty = match &original_function.sig.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, t) => Some(&**t), + } + .ok_or_else(|| { + syn::Error::new_spanned( + original_function.sig.fn_token, + "views must return `Vec` where `T` is a `SpacetimeType`", + ) + })?; + + // Extract the non-context parameters + let arg_tys = arg_tys.iter().skip(1); + + 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::register_anonymous_view::<_, #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::AnonymousViewContextArg>::_ITEM; + let _ = <#ret_ty as spacetimedb::rt::ViewReturn>::_ITEM; + #(let _ = <#arg_tys as spacetimedb::rt::ViewArg>::_ITEM;)* + } + }; + + impl #func_name { + fn invoke(__ctx: spacetimedb::AnonymousViewContext, __args: &[u8]) -> Vec { + spacetimedb::rt::invoke_anonymous_view(#func_name, __ctx, __args) + } + } + + #[automatically_derived] + impl spacetimedb::rt::FnInfo for #func_name { + /// The type of this function + type Invoke = spacetimedb::rt::AnonymousFn; + + /// 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)) + } + } + }) +} + +fn view_impl_client(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 all function parameters, except for `self` ones that aren't allowed. + 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, "`self` arguments not allowed in views")), + }) + .collect::>>()?; + + // Extract all function 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 + let ctx_ty = arg_tys.first().ok_or_else(|| { + syn::Error::new_spanned( + original_function.sig.fn_token, + "A view must have `&ViewContext` as its first argument", + ) + })?; + + // Extract the return type + let ret_ty = match &original_function.sig.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, t) => Some(&**t), + } + .ok_or_else(|| { + syn::Error::new_spanned( + original_function.sig.fn_token, + "views must return `Vec` where `T` is a `SpacetimeType`", + ) + })?; + + // Extract the non-context parameters + let arg_tys = arg_tys.iter().skip(1); + + 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::register_view::<_, #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; + #(let _ = <#arg_tys as spacetimedb::rt::ViewArg>::_ITEM;)* + } + }; + + impl #func_name { + fn invoke(__ctx: spacetimedb::ViewContext, __args: &[u8]) -> Vec { + spacetimedb::rt::invoke_view(#func_name, __ctx, __args) + } + } + + #[automatically_derived] + impl spacetimedb::rt::FnInfo for #func_name { + /// The type of this function + type Invoke = spacetimedb::rt::ViewFn; + + /// 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)) + } + } + }) +} + +pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Result { + if args.anonymous { + view_impl_anon(original_function) + } else { + view_impl_client(original_function) + } +} diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index b7bbb18101d..6d7dfc5ccf5 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -666,6 +666,57 @@ 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` where `T` is a `SpacetimeType`. +/// +/// ```no_run +/// # mod demo { +/// use spacetimedb::{view, table, AnonymousViewContext, ViewContext}; +/// +/// #[table(name = player)] +/// struct Player { +/// #[unique] +/// identity: Identity, +/// #[index(btree)] +/// level: u32, +/// } +/// +/// #[view(public)] +/// pub fn player(ctx: &ViewContext) -> Vec { +/// ctx.db.player().identity().find(ctx.sender).into_iter().collect() +/// } +/// +/// #[view(public, anonymous)] +/// pub fn player(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 6cdddde830d..6087e532241 100644 --- a/crates/bindings/src/rt.rs +++ b/crates/bindings/src/rt.rs @@ -1,14 +1,16 @@ #![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; @@ -43,19 +45,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`", + 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 view signature", + label = "this view signature is not valid", + note = "", + note = "view signatures must match:", + note = " `Fn(&AnonymousViewContext, [T1, ...]) -> Vec`", + 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 +136,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 +186,39 @@ pub trait ReducerArg { } impl ReducerArg for T {} +#[diagnostic::on_unimplemented(message = "A view must have `&ViewContext` as its first argument")] +pub trait ViewContextArg { + #[doc(hidden)] + const _ITEM: () = (); +} +impl ViewContextArg for &ViewContext {} + +#[diagnostic::on_unimplemented(message = "An anonymous view must have `&AnonymousViewContext` as its first argument")] +pub trait AnonymousViewContextArg { + #[doc(hidden)] + const _ITEM: () = (); +} +impl AnonymousViewContextArg 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` where `T` is a `SpacetimeType`")] +pub trait ViewReturn { + #[doc(hidden)] + const _ITEM: () = (); +} +impl ViewReturn for Vec {} + /// 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 +339,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 +364,35 @@ macro_rules! impl_reducer { } } + // Implement `View<..., ViewContext>` for the tuple type `($($T,)*)`. + impl<'de, Func, Elem, $($T),*> + View<'de, ($($T,)*), Elem> for Func + where + $($T: SpacetimeType + Deserialize<'de> + Serialize,)* + Func: Fn(&ViewContext, $($T),*) -> Vec, + Elem: SpacetimeType + Serialize, + { + #[allow(non_snake_case)] + fn invoke(&self, ctx: &ViewContext, args: ($($T,)*)) -> Vec { + let ($($T,)*) = args; + self(ctx, $($T),*) + } + } + + // Implement `View<..., AnonymousViewContext>` for the tuple type `($($T,)*)`. + impl<'de, Func, Elem, $($T),*> + AnonymousView<'de, ($($T,)*), Elem> for Func + where + $($T: SpacetimeType + Deserialize<'de> + Serialize,)* + Func: Fn(&AnonymousViewContext, $($T),*) -> Vec, + Elem: SpacetimeType + Serialize, + { + #[allow(non_snake_case)] + fn invoke(&self, ctx: &AnonymousViewContext, args: ($($T,)*)) -> Vec { + let ($($T,)*) = args; + self(ctx, $($T),*) + } + } }; // Counts the number of elements in the tuple. (@count $($T:ident)*) => { @@ -362,7 +491,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 +499,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, 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, 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 +538,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 +557,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 +593,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); @@ -510,6 +683,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 { @@ -619,7 +867,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 4b704ce4e3f..1d73ad4d15d 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 @@ -254,4 +254,4 @@ 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` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `scheduled_reducer_typecheck` \ No newline at end of file diff --git a/crates/bindings/tests/ui/views.rs b/crates/bindings/tests/ui/views.rs index 131a0cf2f47..32ae227d690 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,62 @@ fn read_only_btree_index_no_delete(ctx: &ReducerContext) { read_only.db.test().x().delete(0u32..); } +#[table(name = player)] +struct Player { + #[unique] + identity: Identity, +} + +/// Private views not allowed; must be `#[view(public)]` +#[view] +fn view_def_no_public(_: &ViewContext) -> 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_1(_: &ReducerContext) -> Vec { + vec![] +} + +/// A `ViewContext` is required +#[view(public)] +fn view_def_wrong_context_2(_: &AnonymousViewContext) -> Vec { + vec![] +} + +/// An `AnonymousViewContext` is required +#[view(public, anonymous)] +fn anonymous_view_def_no_context() -> Vec { + vec![] +} + +/// An `AnonymousViewContext` is required +#[view(public, anonymous)] +fn anonymous_view_def_wrong_context_1(_: &ReducerContext) -> Vec { + vec![] +} + +/// An `AnonymousViewContext` is required +#[view(public, anonymous)] +fn anonymous_view_def_wrong_context_2(_: &ViewContext) -> Vec { + vec![] +} + +/// Must return `Vec` where `T` is a SpacetimeType +#[view(public)] +fn view_def_no_return(_: &ViewContext) {} + +/// Must return `Vec` where `T` is a SpacetimeType +#[view(public)] +fn view_def_wrong_return(_: &ViewContext) -> Option { + None +} + fn main() {} diff --git a/crates/bindings/tests/ui/views.stderr b/crates/bindings/tests/ui/views.stderr index 94dd576c8df..c2de27fff53 100644 --- a/crates/bindings/tests/ui/views.stderr +++ b/crates/bindings/tests/ui/views.stderr @@ -1,3 +1,29 @@ +error: views must be declared as #[view(public)]; public is required + --> tests/ui/views.rs:74:1 + | +74 | #[view] + | ^^^^^^^ + | + = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: A view must have `&ViewContext` as its first argument + --> tests/ui/views.rs:81:1 + | +81 | fn view_def_no_context() -> Vec { + | ^^ + +error: An anonymous view must have `&AnonymousViewContext` as its first argument + --> tests/ui/views.rs:99:1 + | +99 | fn anonymous_view_def_no_context() -> Vec { + | ^^ + +error: views must return `Vec` where `T` is a `SpacetimeType` + --> tests/ui/views.rs:117:1 + | +117 | fn view_def_no_return(_: &ViewContext) {} + | ^^ + error[E0599]: no method named `iter` found for reference `&test__ViewHandle` in the current scope --> tests/ui/views.rs:15:34 | @@ -82,3 +108,253 @@ 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[E0277]: invalid view signature + --> tests/ui/views.rs:87:4 + | + 86 | #[view(public)] + | --------------- required by a bound introduced by this call + 87 | fn view_def_wrong_context_1(_: &ReducerContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `spacetimedb::rt::View<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ReducerContext) -> Vec {view_def_wrong_context_1}` + = note: + = note: view signatures must match: + = note: `Fn(&ViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `register_view` + --> src/rt.rs + | + | pub fn register_view<'a, A, I, T>(_: impl View<'a, A, T>) + | ^^^^^^^^^^^^^^ required by this bound in `register_view` + +error[E0277]: A view must have `&ViewContext` as its first argument + --> tests/ui/views.rs:87:32 + | +87 | fn view_def_wrong_context_1(_: &ReducerContext) -> Vec { + | ^^^^^^^^^^^^^^^ the trait `ViewContextArg` is not implemented for `&ReducerContext` + | + = help: the trait `ViewContextArg` is implemented for `&ViewContext` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:87:4 + | +86 | #[view(public)] + | --------------- required by a bound introduced by this call +87 | fn view_def_wrong_context_1(_: &ReducerContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `spacetimedb::rt::View<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ReducerContext) -> Vec {view_def_wrong_context_1}` + = note: + = note: view signatures must match: + = note: `Fn(&ViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `invoke_view` + --> src/rt.rs + | + | pub fn invoke_view<'a, A: Args<'a>, T: SpacetimeType + Serialize>( + | ----------- required by a bound in this function + | view: impl View<'a, A, T>, + | ^^^^^^^^^^^^^^ required by this bound in `invoke_view` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:93:4 + | + 92 | #[view(public)] + | --------------- required by a bound introduced by this call + 93 | fn view_def_wrong_context_2(_: &AnonymousViewContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `spacetimedb::rt::View<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a AnonymousViewContext) -> Vec {view_def_wrong_context_2}` + = note: + = note: view signatures must match: + = note: `Fn(&ViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `register_view` + --> src/rt.rs + | + | pub fn register_view<'a, A, I, T>(_: impl View<'a, A, T>) + | ^^^^^^^^^^^^^^ required by this bound in `register_view` + +error[E0277]: A view must have `&ViewContext` as its first argument + --> tests/ui/views.rs:93:32 + | +93 | fn view_def_wrong_context_2(_: &AnonymousViewContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^ the trait `ViewContextArg` is not implemented for `&AnonymousViewContext` + | + = help: the trait `ViewContextArg` is implemented for `&ViewContext` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:93:4 + | +92 | #[view(public)] + | --------------- required by a bound introduced by this call +93 | fn view_def_wrong_context_2(_: &AnonymousViewContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `spacetimedb::rt::View<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a AnonymousViewContext) -> Vec {view_def_wrong_context_2}` + = note: + = note: view signatures must match: + = note: `Fn(&ViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `invoke_view` + --> src/rt.rs + | + | pub fn invoke_view<'a, A: Args<'a>, T: SpacetimeType + Serialize>( + | ----------- required by a bound in this function + | view: impl View<'a, A, T>, + | ^^^^^^^^^^^^^^ required by this bound in `invoke_view` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:105:4 + | +104 | #[view(public, anonymous)] + | -------------------------- required by a bound introduced by this call +105 | fn anonymous_view_def_wrong_context_1(_: &ReducerContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `AnonymousView<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ReducerContext) -> Vec {anonymous_view_def_wrong_context_1}` + = note: + = note: view signatures must match: + = note: `Fn(&AnonymousViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `register_anonymous_view` + --> src/rt.rs + | + | pub fn register_anonymous_view<'a, A, I, T>(_: impl AnonymousView<'a, A, T>) + | ^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `register_anonymous_view` + +error[E0277]: An anonymous view must have `&AnonymousViewContext` as its first argument + --> tests/ui/views.rs:105:42 + | +105 | fn anonymous_view_def_wrong_context_1(_: &ReducerContext) -> Vec { + | ^^^^^^^^^^^^^^^ the trait `AnonymousViewContextArg` is not implemented for `&ReducerContext` + | + = help: the trait `AnonymousViewContextArg` is implemented for `&AnonymousViewContext` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:105:4 + | +104 | #[view(public, anonymous)] + | -------------------------- required by a bound introduced by this call +105 | fn anonymous_view_def_wrong_context_1(_: &ReducerContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `AnonymousView<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ReducerContext) -> Vec {anonymous_view_def_wrong_context_1}` + = note: + = note: view signatures must match: + = note: `Fn(&AnonymousViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `invoke_anonymous_view` + --> src/rt.rs + | + 78 | pub fn invoke_anonymous_view<'a, A: Args<'a>, T: SpacetimeType + Serialize>( + | --------------------- required by a bound in this function + 79 | view: impl AnonymousView<'a, A, T>, + | ^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `invoke_anonymous_view` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:111:4 + | +110 | #[view(public, anonymous)] + | -------------------------- required by a bound introduced by this call +111 | fn anonymous_view_def_wrong_context_2(_: &ViewContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `AnonymousView<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ViewContext) -> Vec {anonymous_view_def_wrong_context_2}` + = note: + = note: view signatures must match: + = note: `Fn(&AnonymousViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `register_anonymous_view` + --> src/rt.rs + | + | pub fn register_anonymous_view<'a, A, I, T>(_: impl AnonymousView<'a, A, T>) + | ^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `register_anonymous_view` + +error[E0277]: An anonymous view must have `&AnonymousViewContext` as its first argument + --> tests/ui/views.rs:111:42 + | +111 | fn anonymous_view_def_wrong_context_2(_: &ViewContext) -> Vec { + | ^^^^^^^^^^^^ the trait `AnonymousViewContextArg` is not implemented for `&ViewContext` + | + = help: the trait `AnonymousViewContextArg` is implemented for `&AnonymousViewContext` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:111:4 + | +110 | #[view(public, anonymous)] + | -------------------------- required by a bound introduced by this call +111 | fn anonymous_view_def_wrong_context_2(_: &ViewContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `AnonymousView<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ViewContext) -> Vec {anonymous_view_def_wrong_context_2}` + = note: + = note: view signatures must match: + = note: `Fn(&AnonymousViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `invoke_anonymous_view` + --> src/rt.rs + | + 78 | pub fn invoke_anonymous_view<'a, A: Args<'a>, T: SpacetimeType + Serialize>( + | --------------------- required by a bound in this function + 79 | view: impl AnonymousView<'a, A, T>, + | ^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `invoke_anonymous_view` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:121:4 + | +120 | #[view(public)] + | --------------- required by a bound introduced by this call +121 | fn view_def_wrong_return(_: &ViewContext) -> Option { + | ^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `spacetimedb::rt::View<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ViewContext) -> Option {view_def_wrong_return}` + = note: + = note: view signatures must match: + = note: `Fn(&ViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `register_view` + --> src/rt.rs + | + | pub fn register_view<'a, A, I, T>(_: impl View<'a, A, T>) + | ^^^^^^^^^^^^^^ required by this bound in `register_view` + +error[E0277]: Views must return `Vec` where `T` is a `SpacetimeType` + --> tests/ui/views.rs:121:46 + | +121 | fn view_def_wrong_return(_: &ViewContext) -> Option { + | ^^^^^^^^^^^^^^ the trait `ViewReturn` is not implemented for `Option` + | + = help: the trait `ViewReturn` is implemented for `Vec` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:121:4 + | +120 | #[view(public)] + | --------------- required by a bound introduced by this call +121 | fn view_def_wrong_return(_: &ViewContext) -> Option { + | ^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `spacetimedb::rt::View<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ViewContext) -> Option {view_def_wrong_return}` + = note: + = note: view signatures must match: + = note: `Fn(&ViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `invoke_view` + --> src/rt.rs + | + 50 | pub fn invoke_view<'a, A: Args<'a>, T: SpacetimeType + Serialize>( + | ----------- required by a bound in this function + 51 | view: impl View<'a, A, T>, + | ^^^^^^^^^^^^^^ required by this bound in `invoke_view` diff --git a/crates/core/src/db/relational_db.rs b/crates/core/src/db/relational_db.rs index c7b05a7bbfb..639ae8f0ba4 100644 --- a/crates/core/src/db/relational_db.rs +++ b/crates/core/src/db/relational_db.rs @@ -42,7 +42,7 @@ use spacetimedb_primitives::*; use spacetimedb_sats::algebraic_type::fmt::fmt_algebraic_type; use spacetimedb_sats::memory_usage::MemoryUsage; use spacetimedb_sats::{AlgebraicType, AlgebraicValue, ProductType, ProductValue}; -use spacetimedb_schema::def::{ModuleDef, TableDef}; +use spacetimedb_schema::def::{ModuleDef, TableDef, ViewDef}; use spacetimedb_schema::schema::{ ColumnSchema, IndexSchema, RowLevelSecuritySchema, Schema, SequenceSchema, TableSchema, }; @@ -1055,6 +1055,15 @@ impl RelationalDB { Ok(self.inner.create_table_mut_tx(tx, schema)?) } + pub fn create_view_table( + &self, + tx: &mut MutTx, + module_def: &ModuleDef, + view_def: &ViewDef, + ) -> Result<(ViewId, TableId), DBError> { + Ok(tx.create_view_with_backing_table(module_def, view_def)?) + } + pub fn create_table_for_test_with_the_works( &self, name: &str, diff --git a/crates/core/src/host/module_host.rs b/crates/core/src/host/module_host.rs index d9eade69aeb..7108d842891 100644 --- a/crates/core/src/host/module_host.rs +++ b/crates/core/src/host/module_host.rs @@ -51,7 +51,7 @@ use spacetimedb_query::compile_subscription; use spacetimedb_sats::ProductValue; use spacetimedb_schema::auto_migrate::{AutoMigrateError, MigrationPolicy}; use spacetimedb_schema::def::deserialize::ArgsSeed; -use spacetimedb_schema::def::{ModuleDef, ReducerDef, TableDef}; +use spacetimedb_schema::def::{ModuleDef, ReducerDef, TableDef, ViewDef}; use spacetimedb_schema::schema::{Schema, TableSchema}; use spacetimedb_vm::relation::RelValue; use std::collections::VecDeque; @@ -413,6 +413,18 @@ pub fn create_table_from_def( Ok(()) } +/// Creates the table for `view_def` in `stdb`. +pub fn create_table_from_view_def( + stdb: &RelationalDB, + tx: &mut MutTxId, + module_def: &ModuleDef, + view_def: &ViewDef, +) -> anyhow::Result<()> { + stdb.create_view_table(tx, module_def, view_def) + .with_context(|| format!("failed to create table for view {}", &view_def.name))?; + Ok(()) +} + /// If the module instance's replica_ctx is uninitialized, initialize it. fn init_database( replica_ctx: &ReplicaContext, @@ -436,6 +448,15 @@ fn init_database( logger.info(&format!("Creating table `{}`", &def.name)); create_table_from_def(stdb, tx, module_def, def)?; } + + let mut view_defs: Vec<_> = module_def.views().collect(); + view_defs.sort_by(|a, b| a.name.cmp(&b.name)); + + for def in view_defs { + logger.info(&format!("Creating table for view `{}`", &def.name)); + create_table_from_view_def(stdb, tx, module_def, def)?; + } + // Insert the late-bound row-level security expressions. for rls in module_def.row_level_security() { logger.info(&format!("Creating row level security `{}`", rls.sql)); diff --git a/crates/datastore/src/locking_tx_datastore/committed_state.rs b/crates/datastore/src/locking_tx_datastore/committed_state.rs index 1f17695e135..c3799006d6d 100644 --- a/crates/datastore/src/locking_tx_datastore/committed_state.rs +++ b/crates/datastore/src/locking_tx_datastore/committed_state.rs @@ -6,7 +6,10 @@ use super::{ tx_state::{IndexIdMap, PendingSchemaChange, TxState}, IterByColEqTx, }; -use crate::system_tables::{ST_CONNECTION_CREDENTIALS_ID, ST_CONNECTION_CREDENTIALS_IDX}; +use crate::system_tables::{ + ST_CONNECTION_CREDENTIALS_ID, ST_CONNECTION_CREDENTIALS_IDX, ST_VIEW_COLUMN_ID, ST_VIEW_COLUMN_IDX, ST_VIEW_ID, + ST_VIEW_IDX, ST_VIEW_PARAM_ID, ST_VIEW_PARAM_IDX, +}; use crate::{ db_metrics::DB_METRICS, error::{DatastoreError, IndexError, TableError}, @@ -253,6 +256,10 @@ impl CommittedState { schemas[ST_CONNECTION_CREDENTIALS_IDX].clone(), ); + self.create_table(ST_VIEW_ID, schemas[ST_VIEW_IDX].clone()); + self.create_table(ST_VIEW_PARAM_ID, schemas[ST_VIEW_PARAM_IDX].clone()); + self.create_table(ST_VIEW_COLUMN_ID, schemas[ST_VIEW_COLUMN_IDX].clone()); + // Insert the sequences into `st_sequences` let (st_sequences, blob_store, pool) = self.get_table_and_blob_store_or_create(ST_SEQUENCE_ID, &schemas[ST_SEQUENCE_IDX]); diff --git a/crates/datastore/src/locking_tx_datastore/mut_tx.rs b/crates/datastore/src/locking_tx_datastore/mut_tx.rs index b38cb992d8d..d87afe9d8b3 100644 --- a/crates/datastore/src/locking_tx_datastore/mut_tx.rs +++ b/crates/datastore/src/locking_tx_datastore/mut_tx.rs @@ -8,11 +8,9 @@ use super::{ tx_state::{IndexIdMap, PendingSchemaChange, TxState, TxTableForInsertion}, SharedMutexGuard, SharedWriteGuard, }; -use crate::execution_context::ExecutionContext; -use crate::execution_context::Workload; use crate::system_tables::{ - system_tables, ConnectionIdViaU128, StConnectionCredentialsFields, StConnectionCredentialsRow, - ST_CONNECTION_CREDENTIALS_ID, + system_tables, ConnectionIdViaU128, StConnectionCredentialsFields, StConnectionCredentialsRow, StViewFields, + StViewParamRow, ST_CONNECTION_CREDENTIALS_ID, ST_VIEW_COLUMN_ID, ST_VIEW_ID, }; use crate::traits::{InsertFlags, RowTypeForTable, TxData, UpdateFlags}; use crate::{ @@ -25,6 +23,8 @@ use crate::{ ST_SEQUENCE_ID, ST_TABLE_ID, }, }; +use crate::{execution_context::ExecutionContext, system_tables::StViewColumnRow}; +use crate::{execution_context::Workload, system_tables::StViewRow}; use core::ops::RangeBounds; use core::{cell::RefCell, mem}; use core::{iter, ops::Bound}; @@ -37,7 +37,7 @@ use spacetimedb_lib::{ ConnectionId, Identity, }; use spacetimedb_primitives::{ - col_list, ColId, ColList, ColSet, ConstraintId, IndexId, ScheduleId, SequenceId, TableId, + col_list, ColId, ColList, ColSet, ConstraintId, IndexId, ScheduleId, SequenceId, TableId, ViewId, }; use spacetimedb_sats::{ bsatn::{self, to_writer, DecodeError, Deserializer}, @@ -45,8 +45,9 @@ use spacetimedb_sats::{ ser::Serialize, AlgebraicType, AlgebraicValue, ProductType, ProductValue, WithTypespace, }; -use spacetimedb_schema::schema::{ - ColumnSchema, ConstraintSchema, IndexSchema, RowLevelSecuritySchema, SequenceSchema, TableSchema, +use spacetimedb_schema::{ + def::{ColumnDef, ModuleDef, ViewDef}, + schema::{ColumnSchema, ConstraintSchema, IndexSchema, RowLevelSecuritySchema, SequenceSchema, TableSchema}, }; use spacetimedb_table::{ blob_store::BlobStore, @@ -183,6 +184,41 @@ impl MutTxId { Ok(()) } + pub fn create_view_with_backing_table( + &mut self, + module_def: &ModuleDef, + view_def: &ViewDef, + ) -> Result<(ViewId, TableId)> { + let table_schema = TableSchema::try_from_view_def(module_def, view_def)?; + let table_id = self.create_table(table_schema)?; + + let ViewDef { + name, + is_anonymous, + is_public, + params, + columns, + .. + } = view_def; + + let row = StViewRow { + id: ViewId::SENTINEL, + name: name.clone().into(), + table_id: Some(table_id), + is_public: *is_public, + is_anonymous: *is_anonymous, + }; + let view_id = self + .insert_via_serialize_bsatn(ST_VIEW_ID, &row)? + .1 + .collapse() + .read_col(StViewFields::ViewId)?; + + self.insert_into_st_view_param(view_id, params)?; + self.insert_into_st_view_column(view_id, columns)?; + Ok((view_id, table_id)) + } + /// Create a table. /// /// Requires: @@ -287,6 +323,38 @@ impl MutTxId { }) } + fn insert_into_st_view_param(&mut self, view_id: ViewId, params: &ProductType) -> Result<()> { + for (param_pos, param_name, param_type) in params + .elements + .iter() + .cloned() + .enumerate() + .map(|(i, elem)| (i.into(), elem.name, elem.algebraic_type.into())) + { + let Some(param_name) = param_name else { + return Err(anyhow::anyhow!("hello").into()); + }; + self.insert_via_serialize_bsatn( + ST_VIEW_COLUMN_ID, + &StViewParamRow { + view_id, + param_pos, + param_name, + param_type, + }, + )?; + } + Ok(()) + } + + fn insert_into_st_view_column(&mut self, view_id: ViewId, columns: &[ColumnDef]) -> Result<()> { + for def in columns { + let row = StViewColumnRow::from_column_def(view_id, def.clone()); + self.insert_via_serialize_bsatn(ST_VIEW_COLUMN_ID, &row)?; + } + Ok(()) + } + fn create_table_internal(&mut self, schema: Arc) { // Construct the in memory tables. let table_id = schema.table_id; diff --git a/crates/datastore/src/system_tables.rs b/crates/datastore/src/system_tables.rs index f5515e379b8..e1f5497de20 100644 --- a/crates/datastore/src/system_tables.rs +++ b/crates/datastore/src/system_tables.rs @@ -24,7 +24,7 @@ use spacetimedb_sats::hash::Hash; use spacetimedb_sats::product_value::InvalidFieldError; use spacetimedb_sats::{impl_deserialize, impl_serialize, impl_st, u256, AlgebraicType, AlgebraicValue, ArrayValue}; use spacetimedb_schema::def::{ - BTreeAlgorithm, ConstraintData, DirectAlgorithm, IndexAlgorithm, ModuleDef, UniqueConstraintData, + BTreeAlgorithm, ColumnDef, ConstraintData, DirectAlgorithm, IndexAlgorithm, ModuleDef, UniqueConstraintData, }; use spacetimedb_schema::schema::{ ColumnSchema, ConstraintSchema, IndexSchema, RowLevelSecuritySchema, ScheduleSchema, Schema, SequenceSchema, @@ -65,6 +65,13 @@ pub const ST_ROW_LEVEL_SECURITY_ID: TableId = TableId(10); /// The static ID of the table that stores the credentials for each connection. pub const ST_CONNECTION_CREDENTIALS_ID: TableId = TableId(11); +/// The static ID of the table that tracks views +pub const ST_VIEW_ID: TableId = TableId(12); +/// The static ID of the table that tracks view parameters +pub const ST_VIEW_PARAM_ID: TableId = TableId(13); +/// The static ID of the table that tracks view columns +pub const ST_VIEW_COLUMN_ID: TableId = TableId(14); + pub(crate) const ST_CONNECTION_CREDENTIALS_NAME: &str = "st_connection_credentials"; pub const ST_TABLE_NAME: &str = "st_table"; pub const ST_COLUMN_NAME: &str = "st_column"; @@ -76,6 +83,9 @@ pub(crate) const ST_CLIENT_NAME: &str = "st_client"; pub(crate) const ST_SCHEDULED_NAME: &str = "st_scheduled"; pub(crate) const ST_VAR_NAME: &str = "st_var"; pub(crate) const ST_ROW_LEVEL_SECURITY_NAME: &str = "st_row_level_security"; +pub(crate) const ST_VIEW_NAME: &str = "st_view"; +pub(crate) const ST_VIEW_PARAM_NAME: &str = "st_view_param"; +pub(crate) const ST_VIEW_COLUMN_NAME: &str = "st_view_column"; /// Reserved range of sequence values used for system tables. /// /// Ids for user-created tables will start at `ST_RESERVED_SEQUENCE_RANGE`. @@ -104,7 +114,7 @@ pub enum SystemTable { st_row_level_security, } -pub fn system_tables() -> [TableSchema; 11] { +pub fn system_tables() -> [TableSchema; 14] { [ // The order should match the `id` of the system table, that start with [ST_TABLE_IDX]. st_table_schema(), @@ -118,6 +128,9 @@ pub fn system_tables() -> [TableSchema; 11] { st_row_level_security_schema(), st_sequence_schema(), st_connection_credential_schema(), + st_view_schema(), + st_view_param_schema(), + st_view_column_schema(), ] } @@ -157,6 +170,9 @@ pub(crate) const ST_SCHEDULED_IDX: usize = 7; pub(crate) const ST_ROW_LEVEL_SECURITY_IDX: usize = 8; pub(crate) const ST_SEQUENCE_IDX: usize = 9; pub(crate) const ST_CONNECTION_CREDENTIALS_IDX: usize = 10; +pub(crate) const ST_VIEW_IDX: usize = 11; +pub(crate) const ST_VIEW_PARAM_IDX: usize = 12; +pub(crate) const ST_VIEW_COLUMN_IDX: usize = 13; macro_rules! st_fields_enum { ($(#[$attr:meta])* enum $ty_name:ident { $($name:expr, $var:ident = $discr:expr,)* }) => { @@ -201,6 +217,14 @@ st_fields_enum!(enum StTableFields { "table_primary_key", PrimaryKey = 4, }); // WARNING: For a stable schema, don't change the field names and discriminants. +st_fields_enum!(enum StViewFields { + "view_id", ViewId = 0, + "view_name", ViewName = 1, + "table_id", TableId = 2, + "is_public", IsPublic = 3, + "is_anonymous", IsAnonymous = 4, +}); +// WARNING: For a stable schema, don't change the field names and discriminants. st_fields_enum!(enum StColumnFields { "table_id", TableId = 0, "col_pos", ColPos = 1, @@ -208,6 +232,20 @@ st_fields_enum!(enum StColumnFields { "col_type", ColType = 3, }); // WARNING: For a stable schema, don't change the field names and discriminants. +st_fields_enum!(enum StViewColumnFields { + "view_id", ViewId = 0, + "col_pos", ColPos = 1, + "col_name", ColName = 2, + "col_type", ColType = 3, +}); +// WARNING: For a stable schema, don't change the field names and discriminants. +st_fields_enum!(enum StViewParamFields { + "view_id", ViewId = 0, + "param_pos", ParamPos = 1, + "param_name", ParamName = 2, + "param_type", ParamType = 3, +}); +// WARNING: For a stable schema, don't change the field names and discriminants. st_fields_enum!(enum StIndexFields { "index_id", IndexId = 0, "table_id", TableId = 1, @@ -304,7 +342,16 @@ fn system_module_def() -> ModuleDef { .with_unique_constraint(StTableFields::TableName) .with_index_no_accessor_name(btree(StTableFields::TableName)); - let st_raw_column_type = builder.add_type::(); + let st_view_type = builder.add_type::(); + builder + .build_table(ST_VIEW_NAME, *st_view_type.as_ref().expect("should be ref")) + .with_type(TableType::System) + .with_auto_inc_primary_key(StViewFields::ViewId) + .with_index_no_accessor_name(btree(StViewFields::ViewId)) + .with_unique_constraint(StViewFields::ViewName) + .with_index_no_accessor_name(btree(StViewFields::ViewName)); + + let st_raw_column_type = builder.add_type::(); let st_col_row_unique_cols = [StColumnFields::TableId.col_id(), StColumnFields::ColPos.col_id()]; builder .build_table(ST_COLUMN_NAME, *st_raw_column_type.as_ref().expect("should be ref")) @@ -312,6 +359,22 @@ fn system_module_def() -> ModuleDef { .with_unique_constraint(st_col_row_unique_cols) .with_index_no_accessor_name(btree(st_col_row_unique_cols)); + let st_view_col_type = builder.add_type::(); + let st_view_col_unique_cols = [StViewColumnFields::ViewId.col_id(), StViewColumnFields::ColPos.col_id()]; + builder + .build_table(ST_VIEW_COLUMN_NAME, *st_view_col_type.as_ref().expect("should be ref")) + .with_type(TableType::System) + .with_unique_constraint(st_view_col_unique_cols) + .with_index_no_accessor_name(btree(st_view_col_unique_cols)); + + let st_view_param_type = builder.add_type::(); + let st_view_param_unique_cols = [StViewParamFields::ViewId.col_id(), StViewParamFields::ParamPos.col_id()]; + builder + .build_table(ST_VIEW_PARAM_NAME, *st_view_param_type.as_ref().expect("should be ref")) + .with_type(TableType::System) + .with_unique_constraint(st_view_param_unique_cols) + .with_index_no_accessor_name(btree(st_view_param_unique_cols)); + let st_index_type = builder.add_type::(); builder .build_table(ST_INDEX_NAME, *st_index_type.as_ref().expect("should be ref")) @@ -408,6 +471,9 @@ fn system_module_def() -> ModuleDef { validate_system_table::(&result, ST_VAR_NAME); validate_system_table::(&result, ST_SCHEDULED_NAME); validate_system_table::(&result, ST_CONNECTION_CREDENTIALS_NAME); + validate_system_table::(&result, ST_VIEW_NAME); + validate_system_table::(&result, ST_VIEW_PARAM_NAME); + validate_system_table::(&result, ST_VIEW_COLUMN_NAME); result } @@ -443,6 +509,10 @@ lazy_static::lazy_static! { m.insert("st_scheduled_table_id_key", ConstraintId(10)); m.insert("st_row_level_security_sql_key", ConstraintId(11)); m.insert("st_connection_credentials_connection_id_key", ConstraintId(12)); + m.insert("st_view_view_id_key", ConstraintId(13)); + m.insert("st_view_view_name_key", ConstraintId(14)); + m.insert("st_view_param_view_id_param_pos_key", ConstraintId(15)); + m.insert("st_view_column_view_id_col_pos_key", ConstraintId(16)); m }; } @@ -465,6 +535,10 @@ lazy_static::lazy_static! { m.insert("st_row_level_security_table_id_idx_btree", IndexId(11)); m.insert("st_row_level_security_sql_idx_btree", IndexId(12)); m.insert("st_connection_credentials_connection_id_idx_btree", IndexId(13)); + m.insert("st_view_view_id_idx_btree", IndexId(14)); + m.insert("st_view_view_name_idx_btree", IndexId(15)); + m.insert("st_view_param_view_id_param_pos_idx_btree", IndexId(16)); + m.insert("st_view_column_view_id_col_pos_idx_btree", IndexId(17)); m }; } @@ -479,6 +553,7 @@ lazy_static::lazy_static! { m.insert("st_constraint_constraint_id_seq", SequenceId(3)); m.insert("st_scheduled_schedule_id_seq", SequenceId(4)); m.insert("st_sequence_sequence_id_seq", SequenceId(5)); + m.insert("st_view_view_id_seq", SequenceId(6)); m }; } @@ -579,6 +654,17 @@ pub fn st_var_schema() -> TableSchema { st_schema(ST_VAR_NAME, ST_VAR_ID) } +pub fn st_view_schema() -> TableSchema { + st_schema(ST_VIEW_NAME, ST_VIEW_ID) +} + +pub fn st_view_param_schema() -> TableSchema { + st_schema(ST_VIEW_PARAM_NAME, ST_VIEW_PARAM_ID) +} +pub fn st_view_column_schema() -> TableSchema { + st_schema(ST_VIEW_COLUMN_NAME, ST_VIEW_COLUMN_ID) +} + /// If `table_id` refers to a known system table, return its schema. /// /// Used when restoring from a snapshot; system tables are reinstantiated with this schema, @@ -598,6 +684,9 @@ pub(crate) fn system_table_schema(table_id: TableId) -> Option { ST_CONNECTION_CREDENTIALS_ID => Some(st_connection_credential_schema()), ST_VAR_ID => Some(st_var_schema()), ST_SCHEDULED_ID => Some(st_scheduled_schema()), + ST_VIEW_ID => Some(st_view_schema()), + ST_VIEW_PARAM_ID => Some(st_view_param_schema()), + ST_VIEW_COLUMN_ID => Some(st_view_column_schema()), _ => None, } } @@ -633,6 +722,21 @@ impl From for ProductValue { } } +/// System Table [ST_VIEW_NAME] +/// +/// | view_id | view_name | table_id | is_public | is_anonymous | +/// |---------|-----------|----------|-----------|--------------| +/// | 1 | "player" | 4 | true | true | +#[derive(Debug, Clone, PartialEq, Eq, SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct StViewRow { + pub id: ViewId, + pub name: Box, + pub table_id: Option, + pub is_public: bool, + pub is_anonymous: bool, +} + /// A wrapper around `AlgebraicType` that acts like `AlgegbraicType::bytes()` for serialization purposes. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AlgebraicTypeViaBytes(pub AlgebraicType); @@ -709,6 +813,66 @@ impl From for StColumnRow { } } +/// System Table [ST_VIEW_COLUMN_NAME] +/// +/// | view_id | col_pos | col_name | col_type | +/// |---------|---------|----------|--------------------| +/// | 1 | 0 | "x" | AlgebraicType::U32 | +#[derive(Debug, Clone, PartialEq, Eq, SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct StViewColumnRow { + pub view_id: ViewId, + pub col_pos: ColId, + pub col_name: Box, + pub col_type: AlgebraicTypeViaBytes, +} + +impl StViewColumnRow { + pub fn from_column_schema(view_id: ViewId, schema: ColumnSchema) -> Self { + let ColumnSchema { + col_pos, + col_name, + col_type, + .. + } = schema; + Self { + view_id, + col_pos, + col_name, + col_type: col_type.into(), + } + } + + pub fn from_column_def(view_id: ViewId, def: ColumnDef) -> Self { + let ColumnDef { + name, + col_id: col_pos, + ty, + .. + } = def; + Self { + view_id, + col_pos, + col_name: name.into(), + col_type: ty.into(), + } + } +} + +/// System Table [ST_VIEW_PARAM_NAME] +/// +/// | view_id | param_pos | param_name | param_type | +/// |---------|-----------|------------|-----------------------| +/// | 1 | 0 | "y" | AlgebraicType::U32 | +#[derive(Debug, Clone, PartialEq, Eq, SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct StViewParamRow { + pub view_id: ViewId, + pub param_pos: ColId, + pub param_name: Box, + pub param_type: AlgebraicTypeViaBytes, +} + /// System Table [ST_INDEX_NAME] /// /// | index_id | table_id | index_name | index_algorithm | diff --git a/crates/lib/src/db/raw_def/v9.rs b/crates/lib/src/db/raw_def/v9.rs index 58816384383..b8c01a8b2e3 100644 --- a/crates/lib/src/db/raw_def/v9.rs +++ b/crates/lib/src/db/raw_def/v9.rs @@ -372,13 +372,14 @@ pub struct RawRowLevelSecurityDefV9 { /// If/when we define `RawModuleDefV10`, these should allbe moved out of `misc_exports` and into their own fields. #[derive(Debug, Clone, SpacetimeType)] #[sats(crate = crate)] -#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord, derive_more::From))] #[non_exhaustive] pub enum RawMiscModuleExportV9 { /// A default value for a column added during a supervised automigration. ColumnDefaultValue(RawColumnDefaultValueV9), /// A procedure definition. Procedure(RawProcedureDefV9), + View(RawViewDefV9), } /// Marks a particular table's column as having a particular default. @@ -396,6 +397,38 @@ pub struct RawColumnDefaultValueV9 { pub value: Box<[u8]>, } +/// A view definition. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawViewDefV9 { + /// The name of the view. + pub name: RawIdentifier, + + /// Is this view anonymous? + pub is_anonymous: bool, + + /// Whether this view is public or private. + /// Only public is supported right now. + /// Private views may be added in the future. + pub is_public: bool, + + /// The types and optional names of the parameters, in order. + /// This `ProductType` need not be registered in the typespace. + pub params: ProductType, + + /// A reference to a `ProductType` containing the columns of this view. + /// This is the single source of truth for the view's columns. + /// All elements of the `ProductType` must have names. + /// + /// Like all types in the module, this must have the [default element ordering](crate::db::default_element_ordering), + /// UNLESS a custom ordering is declared via a `RawTypeDefv9` for this type. + pub return_type: AlgebraicType, + + /// Currently unused, but we may want to define indexes on materialized views in the future. + pub indexes: Vec, +} + /// A type declaration. /// /// Exactly of these must be attached to every `Product` and `Sum` type used by a module. @@ -692,6 +725,23 @@ impl RawModuleDefV9Builder { })) } + pub fn add_view( + &mut self, + name: impl Into, + is_anonymous: bool, + params: ProductType, + return_type: AlgebraicType, + ) { + self.module.misc_exports.push(RawMiscModuleExportV9::View(RawViewDefV9 { + name: name.into(), + is_anonymous, + is_public: true, + params, + return_type, + indexes: vec![], + })); + } + /// Add a row-level security policy to the module. /// /// The `sql` expression should be a valid SQL expression that will be used to filter rows. diff --git a/crates/primitives/src/ids.rs b/crates/primitives/src/ids.rs index 77e1352635b..5aadfb8455b 100644 --- a/crates/primitives/src/ids.rs +++ b/crates/primitives/src/ids.rs @@ -78,6 +78,12 @@ system_id! { } auto_inc_system_id!(TableId); +system_id! { + /// An identifier for a view, unique within a database. + pub struct ViewId(pub u32); +} +auto_inc_system_id!(ViewId); + system_id! { /// An identifier for a sequence, unique within a database. pub struct SequenceId(pub u32); diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index d88e541f195..715fc77cec0 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -7,7 +7,9 @@ mod ids; pub use attr::{AttributeKind, ColumnAttribute, ConstraintKind, Constraints}; pub use col_list::{ColList, ColOrCols, ColSet}; -pub use ids::{ColId, ConstraintId, FunctionId, IndexId, ProcedureId, ReducerId, ScheduleId, SequenceId, TableId}; +pub use ids::{ + ColId, ConstraintId, FunctionId, IndexId, ProcedureId, ReducerId, ScheduleId, SequenceId, TableId, ViewId, +}; /// The minimum size of a chunk yielded by a wasm abi RowIter. pub const ROW_ITER_CHUNK_SIZE: usize = 32 * 1024; diff --git a/crates/sats/src/algebraic_type.rs b/crates/sats/src/algebraic_type.rs index c4a45fe1d3b..e42ba015639 100644 --- a/crates/sats/src/algebraic_type.rs +++ b/crates/sats/src/algebraic_type.rs @@ -202,6 +202,11 @@ impl AlgebraicType { matches!(self, Self::Sum(p) if p.is_empty()) } + /// Returns whether this type is an option type. + pub fn is_option(&self) -> bool { + matches!(self, Self::Sum(p) if p.is_option()) + } + /// If this type is the standard option type, returns the type of the `some` variant. /// Otherwise, returns `None`. pub fn as_option(&self) -> Option<&AlgebraicType> { diff --git a/crates/sats/src/de/impls.rs b/crates/sats/src/de/impls.rs index 0e50206703f..cf1090253af 100644 --- a/crates/sats/src/de/impls.rs +++ b/crates/sats/src/de/impls.rs @@ -742,6 +742,7 @@ impl FieldNameVisitor<'_> for TupleNameVisitor<'_> { } impl_deserialize!([] spacetimedb_primitives::TableId, de => u32::deserialize(de).map(Self)); +impl_deserialize!([] spacetimedb_primitives::ViewId, de => u32::deserialize(de).map(Self)); impl_deserialize!([] spacetimedb_primitives::SequenceId, de => u32::deserialize(de).map(Self)); impl_deserialize!([] spacetimedb_primitives::IndexId, de => u32::deserialize(de).map(Self)); impl_deserialize!([] spacetimedb_primitives::ConstraintId, de => u32::deserialize(de).map(Self)); diff --git a/crates/sats/src/ser/impls.rs b/crates/sats/src/ser/impls.rs index 15b2bc67638..dc737c5b50c 100644 --- a/crates/sats/src/ser/impls.rs +++ b/crates/sats/src/ser/impls.rs @@ -258,6 +258,7 @@ impl_serialize!([] ValueWithType<'_, ArrayValue>, (self, ser) => { }); impl_serialize!([] spacetimedb_primitives::TableId, (self, ser) => ser.serialize_u32(self.0)); +impl_serialize!([] spacetimedb_primitives::ViewId, (self, ser) => ser.serialize_u32(self.0)); impl_serialize!([] spacetimedb_primitives::SequenceId, (self, ser) => ser.serialize_u32(self.0)); impl_serialize!([] spacetimedb_primitives::IndexId, (self, ser) => ser.serialize_u32(self.0)); impl_serialize!([] spacetimedb_primitives::ConstraintId, (self, ser) => ser.serialize_u32(self.0)); diff --git a/crates/sats/src/typespace.rs b/crates/sats/src/typespace.rs index 46aeb3ddaa5..36058c79195 100644 --- a/crates/sats/src/typespace.rs +++ b/crates/sats/src/typespace.rs @@ -412,6 +412,7 @@ impl_st!([T] Option, ts => AlgebraicType::option(T::make_type(ts))); impl_st!([] spacetimedb_primitives::ColId, AlgebraicType::U16); impl_st!([] spacetimedb_primitives::TableId, AlgebraicType::U32); +impl_st!([] spacetimedb_primitives::ViewId, AlgebraicType::U32); impl_st!([] spacetimedb_primitives::IndexId, AlgebraicType::U32); impl_st!([] spacetimedb_primitives::SequenceId, AlgebraicType::U32); impl_st!([] spacetimedb_primitives::ConstraintId, AlgebraicType::U32); diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index fb8db9fccf3..eea6f225d75 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -35,7 +35,7 @@ use spacetimedb_lib::db::raw_def::v9::{ Lifecycle, RawColumnDefaultValueV9, RawConstraintDataV9, RawConstraintDefV9, RawIdentifier, RawIndexAlgorithm, RawIndexDefV9, RawMiscModuleExportV9, RawModuleDefV9, RawProcedureDefV9, RawReducerDefV9, RawRowLevelSecurityDefV9, RawScheduleDefV9, RawScopedTypeNameV9, RawSequenceDefV9, RawSql, RawTableDefV9, RawTypeDefV9, - RawUniqueConstraintDataV9, TableAccess, TableType, + RawUniqueConstraintDataV9, RawViewDefV9, TableAccess, TableType, }; use spacetimedb_lib::{ProductType, RawModuleDef}; use spacetimedb_primitives::{ColId, ColList, ColOrCols, ColSet, ProcedureId, ReducerId, TableId}; @@ -109,6 +109,11 @@ pub struct ModuleDef { /// so that `__call_procedure__` receives stable integer IDs. procedures: IndexMap, + /// The views of the module definition. + /// Note: this is using IndexMap because view order is important + /// and must be preserved for future calls to `__call_view__`. + views: IndexMap, + /// A map from lifecycle reducer kind to reducer id. lifecycle_reducers: EnumMap>, @@ -172,6 +177,11 @@ impl ModuleDef { self.procedures.values() } + /// The views of the module definition. + pub fn views(&self) -> impl Iterator { + self.views.values() + } + /// The type definitions of the module definition. pub fn types(&self) -> impl Iterator { self.types.values() @@ -364,6 +374,7 @@ impl From for RawModuleDefV9 { fn from(val: ModuleDef) -> Self { let ModuleDef { tables, + views, reducers, lifecycle_reducers: _, types, @@ -382,7 +393,8 @@ impl From for RawModuleDefV9 { // TODO: Do we need to include default values here? misc_exports: procedures .into_iter() - .map(|(_, def)| RawMiscModuleExportV9::Procedure(def.into())) + .map(|(_, def)| def.into()) + .chain(views.into_iter().map(|(_, def)| def.into())) .collect(), typespace, row_level_security: row_level_security_raw.into_iter().map(|(_, def)| def).collect(), @@ -1030,6 +1042,81 @@ impl From for RawProcedureDefV9 { } } +impl From for RawMiscModuleExportV9 { + fn from(def: ProcedureDef) -> Self { + Self::Procedure(def.into()) + } +} + +/// A view exported by the module. +#[derive(Debug, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub struct ViewDef { + /// The name of the view. This must be unique within the module. + pub name: Identifier, + + /// Is this view anonymous? + pub is_anonymous: bool, + + /// Is this view public or private? + pub is_public: bool, + + /// The parameters of the view. + /// + /// This `ProductType` need not be registered in the module's `Typespace`. + pub params: ProductType, + + /// The parameters of the view, formatted for client codegen. + /// + /// This `ProductType` need not be registered in the module's `TypespaceForGenerate`. + pub params_for_generate: ProductTypeDef, + + /// The return type of the procedure. + /// + /// If this is a non-special compound type, it should be registered in the module's `Typespace` + /// and indirected through an [`AlgebraicType::Ref`]. + pub return_type: AlgebraicType, + + /// The return type of the procedure. + /// + /// If this is a non-special compound type, it should be registered in the module's `TypespaceForGenerate` + /// and indirected through an [`AlgebraicTypeUse::Ref`]. + pub return_type_for_generate: AlgebraicTypeUse, + + /// The columns of this view. This stores the information in + /// `return_type` in a more convenient-to-access format. + pub columns: Vec, +} + +impl From for RawViewDefV9 { + fn from(val: ViewDef) -> Self { + let ViewDef { + name, + is_anonymous, + is_public, + params, + params_for_generate: _, + return_type, + return_type_for_generate: _, + columns: _, + } = val; + RawViewDefV9 { + name: name.into(), + is_anonymous, + is_public, + params, + return_type, + indexes: vec![], + } + } +} + +impl From for RawMiscModuleExportV9 { + fn from(def: ViewDef) -> Self { + Self::View(def.into()) + } +} + impl ModuleDefLookup for TableDef { type Key<'a> = &'a Identifier; @@ -1149,6 +1236,18 @@ impl ModuleDefLookup for ReducerDef { } } +impl ModuleDefLookup for ViewDef { + type Key<'a> = &'a Identifier; + + fn key(&self) -> Self::Key<'_> { + &self.name + } + + fn lookup<'a>(view_def: &'a ModuleDef, key: Self::Key<'_>) -> Option<&'a Self> { + view_def.views.get(key) + } +} + fn to_raw(data: HashMap) -> Vec where Def: ModuleDefLookup + Into, diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index fa7a8b88067..63266bb3bc4 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -5,6 +5,7 @@ use crate::{def::validate::Result, error::TypeLocation}; use spacetimedb_data_structures::error_stream::{CollectAllErrors, CombineErrors}; use spacetimedb_data_structures::map::HashSet; use spacetimedb_lib::db::default_element_ordering::{product_type_has_default_ordering, sum_type_has_default_ordering}; +use spacetimedb_lib::db::raw_def::v9::RawViewDefV9; use spacetimedb_lib::ProductType; use spacetimedb_primitives::col_list; use spacetimedb_sats::{bsatn::de::Deserializer, de::DeserializeSeed, WithTypespace}; @@ -54,13 +55,19 @@ pub fn validate(def: RawModuleDefV9) -> Result { // Later on, in `check_function_names_are_unique`, we'll transform this into an `IndexMap`. .collect_all_errors::>(); - let (procedures, non_procedure_misc_exports) = + let (procedures, misc_exports) = misc_exports .into_iter() .partition::, _>(|misc_export| { matches!(misc_export, RawMiscModuleExportV9::Procedure(_)) }); + let (views, misc_exports) = misc_exports + .into_iter() + .partition::, _>(|misc_export| { + matches!(misc_export, RawMiscModuleExportV9::View(_)) + }); + let procedures = procedures .into_iter() .map(|procedure| { @@ -78,6 +85,21 @@ pub fn validate(def: RawModuleDefV9) -> Result { // Later on, in `check_function_names_are_unique`, we'll transform this into an `IndexMap`. .collect_all_errors::>(); + let views = views + .into_iter() + .map(|view| { + let RawMiscModuleExportV9::View(view) = view else { + unreachable!("Already partitioned views separate from other `RawMiscModuleExportV9` variants"); + }; + view + }) + .map(|view| { + validator + .validate_view_def(view) + .map(|view_def| (view_def.name.clone(), view_def)) + }) + .collect_all_errors(); + let tables = tables .into_iter() .map(|table| { @@ -103,18 +125,17 @@ pub fn validate(def: RawModuleDefV9) -> Result { }) .collect_all_errors::>(); - let tables_types_reducers_procedures = - (tables, types, reducers, procedures) - .combine_errors() - .and_then(|(mut tables, types, reducers, procedures)| { - let ((reducers, procedures), ()) = ( - check_function_names_are_unique(reducers, procedures), - check_non_procedure_misc_exports(non_procedure_misc_exports, &validator, &mut tables), - ) - .combine_errors()?; - check_scheduled_functions_exist(&mut tables, &reducers, &procedures)?; - Ok((tables, types, reducers, procedures)) - }); + let tables_types_reducers_procedures_views = (tables, types, reducers, procedures, views) + .combine_errors() + .and_then(|(mut tables, types, reducers, procedures, views)| { + let ((reducers, procedures, views), ()) = ( + check_function_names_are_unique(reducers, procedures, views), + check_non_procedure_misc_exports(misc_exports, &validator, &mut tables), + ) + .combine_errors()?; + check_scheduled_functions_exist(&mut tables, &reducers, &procedures)?; + Ok((tables, types, reducers, procedures, views)) + }); let ModuleValidator { stored_in_table_def, @@ -123,14 +144,15 @@ pub fn validate(def: RawModuleDefV9) -> Result { .. } = validator; - let (tables, types, reducers, procedures) = - (tables_types_reducers_procedures).map_err(|errors| errors.sort_deduplicate())?; + let (tables, types, reducers, procedures, views) = + (tables_types_reducers_procedures_views).map_err(|errors| errors.sort_deduplicate())?; let typespace_for_generate = typespace_for_generate.finish(); Ok(ModuleDef { tables, reducers, + views, types, typespace, typespace_for_generate, @@ -426,6 +448,89 @@ impl ModuleValidator<'_> { }) } + /// Validate a view definition. + fn validate_view_def(&mut self, view_def: RawViewDefV9) -> Result { + let RawViewDefV9 { + name, + is_anonymous, + is_public, + params, + return_type, + indexes: _, + } = view_def; + + let params_for_generate = self.params_for_generate(¶ms, |position, arg_name| TypeLocation::ViewArg { + view_name: Cow::Borrowed(&name), + position, + arg_name, + }); + + let return_type_for_generate = self.validate_for_type_use( + &TypeLocation::ViewReturn { + view_name: Cow::Borrowed(&name), + }, + &return_type, + ); + + // We exit early if we don't find the product type ref, + // since this breaks all the other checks. + let product_type_ref = return_type + .as_array() + .and_then(|array_type| match *array_type.elem_ty { + AlgebraicType::Ref(ref_type) => Some(ref_type), + _ => None, + }) + .ok_or_else(|| { + ValidationErrors::from(ValidationError::InvalidViewReturnType { + view: name.clone(), + ty: return_type.clone().into(), + }) + })?; + + let product_type = self + .typespace + .get(product_type_ref) + .and_then(AlgebraicType::as_product) + .ok_or_else(|| { + ValidationErrors::from(ValidationError::InvalidProductTypeRef { + table: name.clone(), + ref_: product_type_ref, + }) + })?; + + let mut table_in_progress = TableValidator { + raw_name: name.clone(), + product_type_ref, + product_type, + module_validator: self, + has_sequence: Default::default(), + }; + + let columns = (0..product_type.elements.len()) + .map(|id| table_in_progress.validate_column_def(id.into())) + .collect_all_errors(); + + // views don't live in the global namespace. + let name = identifier(name); + + let (name, params_for_generate, return_type_for_generate, columns) = + (name, params_for_generate, return_type_for_generate, columns).combine_errors()?; + + Ok(ViewDef { + name, + is_anonymous, + is_public, + params, + params_for_generate: ProductTypeDef { + elements: params_for_generate, + recursive: false, // A ProductTypeDef not stored in a Typespace cannot be recursive. + }, + return_type, + return_type_for_generate, + columns, + }) + } + fn validate_column_default_value( &self, tables: &HashMap, @@ -1033,10 +1138,16 @@ fn check_scheduled_functions_exist( /// Check that all function (reducer and procedure) names are unique, /// then re-organize the reducers and procedures into [`IndexMap`]s /// for storage in the [`ModuleDef`]. +#[allow(clippy::type_complexity)] fn check_function_names_are_unique( reducers: Vec<(Identifier, ReducerDef)>, procedures: Vec<(Identifier, ProcedureDef)>, -) -> Result<(IndexMap, IndexMap)> { + views: Vec<(Identifier, ViewDef)>, +) -> Result<( + IndexMap, + IndexMap, + IndexMap, +)> { let mut errors = vec![]; let mut reducers_map = IndexMap::with_capacity(reducers.len()); @@ -1059,7 +1170,17 @@ fn check_function_names_are_unique( } } - ErrorStream::add_extra_errors(Ok((reducers_map, procedures_map)), errors) + let mut views_map = IndexMap::with_capacity(views.len()); + + for (name, def) in views { + if reducers_map.contains_key(&name) || procedures_map.contains_key(&name) || views_map.contains_key(&name) { + errors.push(ValidationError::DuplicateFunctionName { name }); + } else { + views_map.insert(name, def); + } + } + + ErrorStream::add_extra_errors(Ok((reducers_map, procedures_map, views_map)), errors) } fn check_non_procedure_misc_exports( diff --git a/crates/schema/src/error.rs b/crates/schema/src/error.rs index f7d05c8febf..b5ee5ee9d29 100644 --- a/crates/schema/src/error.rs +++ b/crates/schema/src/error.rs @@ -82,6 +82,11 @@ pub enum ValidationError { start: Option, max_value: Option, }, + #[error("View {view} has invalid return type {ty}")] + InvalidViewReturnType { + view: RawIdentifier, + ty: PrettyAlgebraicType, + }, #[error("Table {table} has invalid product_type_ref {ref_}")] InvalidProductTypeRef { table: RawIdentifier, @@ -182,8 +187,16 @@ pub enum TypeLocation<'a> { position: usize, arg_name: Option>, }, + /// A view argument. + ViewArg { + view_name: Cow<'a, str>, + position: usize, + arg_name: Option>, + }, /// A procedure return type. ProcedureReturn { procedure_name: Cow<'a, str> }, + /// A view return type. + ViewReturn { view_name: Cow<'a, str> }, /// A type in the typespace. InTypespace { /// The reference to the type within the typespace. @@ -213,9 +226,21 @@ impl TypeLocation<'_> { position, arg_name: arg_name.map(|s| s.to_string().into()), }, + TypeLocation::ViewArg { + view_name, + position, + arg_name, + } => TypeLocation::ViewArg { + view_name: view_name.to_string().into(), + position, + arg_name: arg_name.map(|s| s.to_string().into()), + }, Self::ProcedureReturn { procedure_name } => TypeLocation::ProcedureReturn { procedure_name: procedure_name.to_string().into(), }, + Self::ViewReturn { view_name } => TypeLocation::ViewReturn { + view_name: view_name.to_string().into(), + }, // needed to convince rustc this is allowed. TypeLocation::InTypespace { ref_ } => TypeLocation::InTypespace { ref_ }, } @@ -247,9 +272,23 @@ impl fmt::Display for TypeLocation<'_> { } Ok(()) } + TypeLocation::ViewArg { + view_name, + position, + arg_name, + } => { + write!(f, "view `{view_name}` argument {position}")?; + if let Some(arg_name) = arg_name { + write!(f, " (`{arg_name}`)")?; + } + Ok(()) + } TypeLocation::ProcedureReturn { procedure_name } => { write!(f, "procedure `{procedure_name}` return value") } + TypeLocation::ViewReturn { view_name } => { + write!(f, "view `{view_name}` return value") + } TypeLocation::InTypespace { ref_ } => { write!(f, "typespace ref `{ref_}`") } diff --git a/crates/schema/src/schema.rs b/crates/schema/src/schema.rs index 149cb08eb5b..04e3b488624 100644 --- a/crates/schema/src/schema.rs +++ b/crates/schema/src/schema.rs @@ -8,6 +8,7 @@ use crate::def::error::{DefType, SchemaError}; use crate::relation::{combine_constraints, Column, DbTable, FieldName, Header}; +use anyhow::bail; use core::mem; use itertools::Itertools; use spacetimedb_lib::db::auth::{StAccess, StTableType}; @@ -21,7 +22,7 @@ use std::sync::Arc; use crate::def::{ ColumnDef, ConstraintData, ConstraintDef, IndexAlgorithm, IndexDef, ModuleDef, ModuleDefLookup, ScheduleDef, - SequenceDef, TableDef, UniqueConstraintData, + SequenceDef, TableDef, UniqueConstraintData, ViewDef, }; use crate::identifier::Identifier; @@ -587,6 +588,64 @@ pub fn column_schemas_from_defs(module_def: &ModuleDef, columns: &[ColumnDef], t .collect() } +impl TableSchema { + pub fn try_from_view_def(module_def: &ModuleDef, view_def: &ViewDef) -> anyhow::Result { + module_def.expect_contains(view_def); + + let ViewDef { + name, + is_anonymous, + is_public, + params, + params_for_generate: _, + return_type: _, + return_type_for_generate: _, + columns, + } = view_def; + + let mut columns = column_schemas_from_defs(module_def, columns, TableId::SENTINEL); + let n = columns.len(); + + for (i, elem) in params.elements.iter().cloned().enumerate() { + let Some(col_name) = elem.name else { bail!("hello") }; + columns.push(ColumnSchema { + table_id: TableId::SENTINEL, + col_pos: (n + i).into(), + col_name, + col_type: elem.algebraic_type, + }); + } + + if !is_anonymous { + columns.push(ColumnSchema { + table_id: TableId::SENTINEL, + col_pos: columns.len().into(), + col_name: "sender".into(), + col_type: AlgebraicType::identity(), + }); + } + + let table_access = if *is_public { + StAccess::Public + } else { + StAccess::Private + }; + + Ok(TableSchema::new( + TableId::SENTINEL, + (*name).clone().into(), + columns, + vec![], + vec![], + vec![], + StTableType::User, + table_access, + None, + None, + )) + } +} + impl Schema for TableSchema { type Def = TableDef; type Id = TableId; diff --git a/crates/table/src/read_column.rs b/crates/table/src/read_column.rs index 123b751f5a3..be58029f714 100644 --- a/crates/table/src/read_column.rs +++ b/crates/table/src/read_column.rs @@ -326,6 +326,7 @@ macro_rules! impl_read_column_via_from { impl_read_column_via_from! { u16 => spacetimedb_primitives::ColId; + u32 => spacetimedb_primitives::ViewId; u32 => spacetimedb_primitives::TableId; u32 => spacetimedb_primitives::IndexId; u32 => spacetimedb_primitives::ConstraintId;