Skip to content

Commit 9f59118

Browse files
Generate read-only table handles for #[table] (#3364)
# Description of Changes <!-- Please describe your change, mention any related tickets, and so on here. --> The `#[table]` macro now generates read-only table and index handles. # API and ABI breaking changes <!-- If this is an API or ABI breaking change, please apply the corresponding GitHub label. --> None # Expected complexity level and risk <!-- How complicated do you think these changes are? Grade on a scale from 1 to 5, where 1 is a trivial change, and 5 is a deep-reaching and complex change. This complexity rating applies not only to the complexity apparent in the diff, but also to its interactions with existing and future code. If you answered more than a 2, explain what is complex about the PR, and what other components it interacts with in potentially concerning ways. --> 1 # Testing <!-- Describe any testing you've done, and any testing you'd like your reviewers to do, so that you're confident that all the changes work as expected! --> - [x] positive and negative test cases using `ReducerContext::as_read_only()`
1 parent e3d2dfd commit 9f59118

File tree

6 files changed

+456
-57
lines changed

6 files changed

+456
-57
lines changed

crates/bindings-macro/src/table.rs

Lines changed: 111 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,41 @@ impl IndexArg {
275275
}
276276
}
277277

278+
enum AccessorType {
279+
Read,
280+
ReadWrite,
281+
}
282+
283+
impl AccessorType {
284+
fn unique(&self) -> proc_macro2::TokenStream {
285+
match self {
286+
AccessorType::Read => quote!(spacetimedb::UniqueColumnReadOnly),
287+
AccessorType::ReadWrite => quote!(spacetimedb::UniqueColumn),
288+
}
289+
}
290+
291+
fn range(&self) -> proc_macro2::TokenStream {
292+
match self {
293+
AccessorType::Read => quote!(spacetimedb::RangedIndexReadOnly),
294+
AccessorType::ReadWrite => quote!(spacetimedb::RangedIndex),
295+
}
296+
}
297+
298+
fn unique_doc_typename(&self) -> &'static str {
299+
match self {
300+
AccessorType::Read => "UniqueColumnReadOnly",
301+
AccessorType::ReadWrite => "UniqueColumn",
302+
}
303+
}
304+
305+
fn range_doc_typename(&self) -> &'static str {
306+
match self {
307+
AccessorType::Read => "RangedIndexReadOnly",
308+
AccessorType::ReadWrite => "RangedIndex",
309+
}
310+
}
311+
}
312+
278313
struct ValidatedIndex<'a> {
279314
index_name: String,
280315
accessor_name: &'a Ident,
@@ -312,46 +347,71 @@ impl ValidatedIndex<'_> {
312347
})
313348
}
314349

315-
fn accessor(&self, vis: &syn::Visibility, row_type_ident: &Ident) -> TokenStream {
350+
fn accessor(
351+
&self,
352+
vis: &syn::Visibility,
353+
row_type_ident: &Ident,
354+
tbl_type_ident: &Ident,
355+
flavor: AccessorType,
356+
) -> TokenStream {
316357
let cols = match &self.kind {
317358
ValidatedIndexType::BTree { cols } => &**cols,
318359
ValidatedIndexType::Direct { col } => slice::from_ref(col),
319360
};
320361
if self.is_unique {
321362
assert_eq!(cols.len(), 1);
322-
let col = cols[0];
323-
self.accessor_unique(col, row_type_ident)
363+
self.unique_accessor(cols[0], row_type_ident, tbl_type_ident, flavor)
324364
} else {
325-
self.accessor_general(vis, row_type_ident, cols)
365+
self.range_accessor(vis, row_type_ident, tbl_type_ident, cols, flavor)
326366
}
327367
}
328368

329-
fn accessor_unique(&self, col: &Column<'_>, row_type_ident: &Ident) -> TokenStream {
369+
fn unique_accessor(
370+
&self,
371+
col: &Column<'_>,
372+
row_type_ident: &Ident,
373+
tbl_type_ident: &Ident,
374+
flavor: AccessorType,
375+
) -> TokenStream {
330376
let index_ident = self.accessor_name;
331377
let vis = col.vis;
332378
let col_ty = col.ty;
333379
let column_ident = col.ident;
334380

381+
let unique_ty = flavor.unique();
382+
let tbl_token = quote!(#tbl_type_ident);
383+
let doc_type = flavor.unique_doc_typename();
384+
335385
let doc = format!(
336-
"Gets the [`UniqueColumn`][spacetimedb::UniqueColumn] for the \
386+
"Gets the [`{doc_type}`][spacetimedb::{doc_type}] for the \
337387
[`{column_ident}`][{row_type_ident}::{column_ident}] column."
338388
);
339389
quote! {
340390
#[doc = #doc]
341-
#vis fn #column_ident(&self) -> spacetimedb::UniqueColumn<Self, #col_ty, __indices::#index_ident> {
342-
spacetimedb::UniqueColumn::__NEW
391+
#vis fn #column_ident(&self) -> #unique_ty<#tbl_token, #col_ty, __indices::#index_ident> {
392+
#unique_ty::__NEW
343393
}
344394
}
345395
}
346396

347-
fn accessor_general(&self, vis: &syn::Visibility, row_type_ident: &Ident, cols: &[&Column<'_>]) -> TokenStream {
397+
fn range_accessor(
398+
&self,
399+
vis: &syn::Visibility,
400+
row_type_ident: &Ident,
401+
tbl_type_ident: &Ident,
402+
cols: &[&Column<'_>],
403+
flavor: AccessorType,
404+
) -> TokenStream {
348405
let index_ident = self.accessor_name;
349-
let col_tys = cols.iter().map(|col| col.ty);
406+
let col_tys = cols.iter().map(|c| c.ty);
407+
408+
let range_ty = flavor.range();
409+
let tbl_token = quote!(#tbl_type_ident);
410+
let doc_type = flavor.range_doc_typename();
411+
350412
let mut doc = format!(
351-
"Gets the `{index_ident}` [`RangedIndex`][spacetimedb::RangedIndex] as defined \
352-
on this table. \n\
353-
\n\
354-
This B-tree index is defined on the following columns, in order:\n"
413+
"Gets the `{index_ident}` [`{doc_type}`][spacetimedb::{doc_type}] as defined \
414+
on this table.\n\nThis B-tree index is defined on the following columns, in order:\n"
355415
);
356416
for col in cols {
357417
use std::fmt::Write;
@@ -363,10 +423,11 @@ impl ValidatedIndex<'_> {
363423
)
364424
.unwrap();
365425
}
426+
366427
quote! {
367428
#[doc = #doc]
368-
#vis fn #index_ident(&self) -> spacetimedb::RangedIndex<Self, (#(#col_tys,)*), __indices::#index_ident> {
369-
spacetimedb::RangedIndex::__NEW
429+
#vis fn #index_ident(&self) -> #range_ty<#tbl_token, (#(#col_tys,)*), __indices::#index_ident> {
430+
#range_ty::__NEW
370431
}
371432
}
372433
}
@@ -521,6 +582,7 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R
521582

522583
let original_struct_ident = sats_ty.ident;
523584
let table_ident = &args.name;
585+
let view_trait_ident = format_ident!("{}__view", table_ident);
524586
let table_name = table_ident.unraw().to_string();
525587
let sats::SatsTypeData::Product(fields) = &sats_ty.data else {
526588
return Err(syn::Error::new(Span::call_site(), "spacetimedb table must be a struct"));
@@ -656,9 +718,15 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R
656718
indices.sort_by_key(|index| !index.is_unique);
657719

658720
let tablehandle_ident = format_ident!("{}__TableHandle", table_ident);
721+
let viewhandle_ident = format_ident!("{}__ViewHandle", table_ident);
659722

660723
let index_descs = indices.iter().map(|index| index.desc());
661-
let index_accessors = indices.iter().map(|index| index.accessor(vis, original_struct_ident));
724+
let index_accessors_rw = indices
725+
.iter()
726+
.map(|index| index.accessor(vis, original_struct_ident, &tablehandle_ident, AccessorType::ReadWrite));
727+
let index_accessors_ro = indices
728+
.iter()
729+
.map(|index| index.accessor(vis, original_struct_ident, &tablehandle_ident, AccessorType::Read));
662730
let index_marker_types = indices.iter().map(|index| index.marker_type(vis, &tablehandle_ident));
663731

664732
// Generate `integrate_generated_columns`
@@ -831,12 +899,31 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R
831899
}
832900
};
833901

902+
let trait_def_view = quote_spanned! {table_ident.span()=>
903+
#[allow(non_camel_case_types, dead_code)]
904+
#vis trait #view_trait_ident {
905+
fn #table_ident(&self) -> &#viewhandle_ident;
906+
}
907+
impl #view_trait_ident for spacetimedb::LocalReadOnly {
908+
#[inline]
909+
fn #table_ident(&self) -> &#viewhandle_ident {
910+
&#viewhandle_ident {}
911+
}
912+
}
913+
};
914+
834915
let tablehandle_def = quote! {
835916
#[allow(non_camel_case_types)]
836917
#[non_exhaustive]
837918
#vis struct #tablehandle_ident {}
838919
};
839920

921+
let viewhandle_def = quote! {
922+
#[allow(non_camel_case_types)]
923+
#[non_exhaustive]
924+
#vis struct #viewhandle_ident {}
925+
};
926+
840927
let emission = quote! {
841928
const _: () = {
842929
#(let _ = <#field_types as spacetimedb::rt::TableColumn>::_ITEM;)*
@@ -845,12 +932,18 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R
845932
};
846933

847934
#trait_def
935+
#trait_def_view
848936

849937
#tablehandle_def
938+
#viewhandle_def
850939

851940
const _: () = {
852941
impl #tablehandle_ident {
853-
#(#index_accessors)*
942+
#(#index_accessors_rw)*
943+
}
944+
945+
impl #viewhandle_ident {
946+
#(#index_accessors_ro)*
854947
}
855948

856949
#tabletype_impl

crates/bindings/src/lib.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ pub use spacetimedb_lib::TimeDuration;
4141
pub use spacetimedb_lib::Timestamp;
4242
pub use spacetimedb_primitives::TableId;
4343
pub use sys::Errno;
44-
pub use table::{AutoIncOverflow, RangedIndex, Table, TryInsertError, UniqueColumn, UniqueConstraintViolation};
44+
pub use table::{
45+
AutoIncOverflow, RangedIndex, RangedIndexReadOnly, Table, TryInsertError, UniqueColumn, UniqueColumnReadOnly,
46+
UniqueConstraintViolation,
47+
};
4548

4649
pub type ReducerResult = core::result::Result<(), Box<str>>;
4750

@@ -663,6 +666,22 @@ pub use spacetimedb_bindings_macro::table;
663666
#[doc(inline)]
664667
pub use spacetimedb_bindings_macro::reducer;
665668

669+
/// One of two possible types that can be passed as the first argument to a `#[view]`.
670+
/// The other is [`ViewContext`].
671+
/// Use this type if the view does not depend on the caller's identity.
672+
pub struct AnonymousViewContext {
673+
pub db: LocalReadOnly,
674+
}
675+
676+
/// One of two possible types that can be passed as the first argument to a `#[view]`.
677+
/// The other is [`AnonymousViewContext`].
678+
/// Use this type if the view depends on the caller's identity.
679+
pub struct ViewContext {
680+
pub sender: Identity,
681+
pub connection_id: Option<ConnectionId>,
682+
pub db: LocalReadOnly,
683+
}
684+
666685
/// The context that any reducer is provided with.
667686
///
668687
/// This must be the first argument of the reducer. Clients of the module will
@@ -760,6 +779,20 @@ impl ReducerContext {
760779
// which reads the module identity out of the `InstanceEnv`.
761780
Identity::from_byte_array(spacetimedb_bindings_sys::identity())
762781
}
782+
783+
/// Create an anonymous (no sender) read-only view context
784+
pub fn as_anonymous_read_only(&self) -> AnonymousViewContext {
785+
AnonymousViewContext { db: LocalReadOnly {} }
786+
}
787+
788+
/// Create a sender-bound read-only view context using this reducer's caller.
789+
pub fn as_read_only(&self) -> ViewContext {
790+
ViewContext {
791+
sender: self.sender,
792+
connection_id: self.connection_id,
793+
db: LocalReadOnly {},
794+
}
795+
}
763796
}
764797

765798
/// A handle on a database with a particular table schema.
@@ -796,6 +829,10 @@ impl DbContext for ReducerContext {
796829
#[non_exhaustive]
797830
pub struct Local {}
798831

832+
/// The read-only version of [`Local`]
833+
#[non_exhaustive]
834+
pub struct LocalReadOnly {}
835+
799836
// #[cfg(target_arch = "wasm32")]
800837
// #[global_allocator]
801838
// static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

0 commit comments

Comments
 (0)