diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs index 25385d3056d..dc112b7b2b8 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs @@ -798,6 +798,29 @@ public bool Delete(global::TestUniqueNotEquatable row) => TestUniqueNotEquatable, global::TestUniqueNotEquatable >.DoDelete(row); + + public sealed class PrimaryKeyFieldUniqueIndex + : UniqueIndex< + TestUniqueNotEquatable, + global::TestUniqueNotEquatable, + TestEnumWithExplicitValues, + SpacetimeDB.BSATN.Enum + > + { + internal PrimaryKeyFieldUniqueIndex() + : base("TestUniqueNotEquatable_PrimaryKeyField_idx_btree") { } + + // Important: don't move this to the base class. + // C# generics don't play well with nullable types and can't accept both struct-type-based and class-type-based + // `globalName` in one generic definition, leading to buggy `Row?` expansion for either one or another. + public global::TestUniqueNotEquatable? Find(TestEnumWithExplicitValues key) => + DoFilter(key).Cast().SingleOrDefault(); + + public global::TestUniqueNotEquatable Update(global::TestUniqueNotEquatable row) => + DoUpdate(row); + } + + public PrimaryKeyFieldUniqueIndex PrimaryKeyField => new(); } } diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module.verified.txt b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module.verified.txt index 886b2360e60..66f3671d130 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module.verified.txt +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module.verified.txt @@ -52,23 +52,6 @@ } }, {/* - [PrimaryKey] - public TestEnumWithExplicitValues PrimaryKeyField; - ^^^^^^^^^^^^^^^ -} -*/ - Message: Field PrimaryKeyField is marked as Unique but it has a type TestEnumWithExplicitValues which is not an equatable primitive., - Severity: Error, - Descriptor: { - Id: STDB0003, - Title: Unique fields must be equatable, - MessageFormat: Field {0} is marked as Unique but it has a type {1} which is not an equatable primitive., - Category: SpacetimeDB, - DefaultSeverity: Error, - IsEnabledByDefault: true - } - }, - {/* [SpacetimeDB.Table] public partial record TestTableTaggedEnum : SpacetimeDB.TaggedEnum<(int X, int Y)> { } ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -429,4 +412,4 @@ public partial struct TestScheduleIssues } } ] -} \ No newline at end of file +} diff --git a/crates/bindings-csharp/Codegen/Module.cs b/crates/bindings-csharp/Codegen/Module.cs index 0fedf022d58..7c9e6fc9d4b 100644 --- a/crates/bindings-csharp/Codegen/Module.cs +++ b/crates/bindings-csharp/Codegen/Module.cs @@ -96,9 +96,32 @@ or SpecialType.System_Int64 diag.Report(ErrorDescriptor.AutoIncNotInteger, field); } + // Check whether this is a sum type without a payload. + var isAllUnitEnum = false; + if (type.TypeKind == Microsoft.CodeAnalysis.TypeKind.Enum) + { + isAllUnitEnum = true; + } + else if (type.BaseType?.OriginalDefinition.ToString() == "SpacetimeDB.TaggedEnum") + { + if ( + type.BaseType.TypeArguments.FirstOrDefault() is INamedTypeSymbol + { + IsTupleType: true, + TupleElements: var taggedEnumVariants + } + ) + { + isAllUnitEnum = taggedEnumVariants.All( + (field) => field.Type.ToString() == "SpacetimeDB.Unit" + ); + } + } + IsEquatable = ( isInteger + || isAllUnitEnum || type.SpecialType switch { SpecialType.System_String or SpecialType.System_Boolean => true, diff --git a/crates/bindings-csharp/Runtime/Internal/Bounds.cs b/crates/bindings-csharp/Runtime/Internal/Bounds.cs index 4de9ac851fc..6e0a931104b 100644 --- a/crates/bindings-csharp/Runtime/Internal/Bounds.cs +++ b/crates/bindings-csharp/Runtime/Internal/Bounds.cs @@ -18,7 +18,6 @@ public interface IBTreeIndexBounds } public readonly struct Bound(T min, T max) - where T : IEquatable { public T Min => min; public T Max => max; @@ -29,7 +28,6 @@ public readonly struct Bound(T min, T max) } public readonly struct BTreeIndexBounds(Bound t) : IBTreeIndexBounds - where T : IEquatable where TRW : struct, IReadWrite { public ushort PrefixElems => 0; @@ -50,7 +48,6 @@ public void REnd(BinaryWriter w) } public readonly struct BTreeIndexBounds((T t, Bound u) b) : IBTreeIndexBounds - where U : IEquatable where TRW : struct, IReadWrite where URW : struct, IReadWrite { @@ -76,7 +73,6 @@ public void REnd(BinaryWriter w) public readonly struct BTreeIndexBounds((T t, U u, Bound v) b) : IBTreeIndexBounds - where V : IEquatable where TRW : struct, IReadWrite where URW : struct, IReadWrite where VRW : struct, IReadWrite @@ -105,7 +101,6 @@ public void REnd(BinaryWriter w) public readonly struct BTreeIndexBounds( (T t, U u, V v, Bound w) b ) : IBTreeIndexBounds - where W : IEquatable where TRW : struct, IReadWrite where URW : struct, IReadWrite where VRW : struct, IReadWrite @@ -136,7 +131,6 @@ public void REnd(BinaryWriter w) public readonly struct BTreeIndexBounds( (T t, U u, V v, W w, Bound x) b ) : IBTreeIndexBounds - where X : IEquatable where TRW : struct, IReadWrite where URW : struct, IReadWrite where VRW : struct, IReadWrite @@ -169,7 +163,6 @@ public void REnd(BinaryWriter w) public readonly struct BTreeIndexBounds( (T t, U u, V v, W w, X x, Bound y) b ) : IBTreeIndexBounds - where Y : IEquatable where TRW : struct, IReadWrite where URW : struct, IReadWrite where VRW : struct, IReadWrite @@ -204,7 +197,6 @@ public void REnd(BinaryWriter w) public readonly struct BTreeIndexBounds( (T t, U u, V v, W w, X x, Y y, Bound z) b ) : IBTreeIndexBounds - where Z : IEquatable where TRW : struct, IReadWrite where URW : struct, IReadWrite where VRW : struct, IReadWrite @@ -256,7 +248,6 @@ public readonly struct BTreeIndexBounds< A, ARW >((T t, U u, V v, W w, X x, Y y, Z z, Bound a) b) : IBTreeIndexBounds - where A : IEquatable where TRW : struct, IReadWrite where URW : struct, IReadWrite where VRW : struct, IReadWrite @@ -312,7 +303,6 @@ public readonly struct BTreeIndexBounds< B, BRW >((T t, U u, V v, W w, X x, Y y, Z z, A a, Bound b) b) : IBTreeIndexBounds - where B : IEquatable where TRW : struct, IReadWrite where URW : struct, IReadWrite where VRW : struct, IReadWrite @@ -372,7 +362,6 @@ public readonly struct BTreeIndexBounds< C, CRW >((T t, U u, V v, W w, X x, Y y, Z z, A a, B b, Bound c) b) : IBTreeIndexBounds - where C : IEquatable where TRW : struct, IReadWrite where URW : struct, IReadWrite where VRW : struct, IReadWrite diff --git a/crates/bindings-csharp/Runtime/Internal/IIndex.cs b/crates/bindings-csharp/Runtime/Internal/IIndex.cs index bde3b76c5ab..800a119133b 100644 --- a/crates/bindings-csharp/Runtime/Internal/IIndex.cs +++ b/crates/bindings-csharp/Runtime/Internal/IIndex.cs @@ -85,7 +85,6 @@ out handle public abstract class UniqueIndex(string name) : IndexBase(name) where Handle : ITableView where Row : IStructuralReadWrite, new() - where T : IEquatable where RW : struct, BSATN.IReadWrite { private static BTreeIndexBounds ToBounds(T key) => new(key); diff --git a/crates/bindings-macro/src/sats.rs b/crates/bindings-macro/src/sats.rs index 7e49f002603..18967514f08 100644 --- a/crates/bindings-macro/src/sats.rs +++ b/crates/bindings-macro/src/sats.rs @@ -143,6 +143,7 @@ pub(crate) fn derive_satstype(ty: &SatsType<'_>) -> TokenStream { let name = &ty.ident; let krate = &ty.krate; + let mut add_impls_for_plain_enum = false; let typ = match &ty.data { SatsTypeData::Product(fields) => { let fields = fields.iter().map(|field| { @@ -166,6 +167,10 @@ pub(crate) fn derive_satstype(ty: &SatsType<'_>) -> TokenStream { ) } SatsTypeData::Sum(variants) => { + // To allow an enum, with all-unit variants, as an index key type, + // add derive `Filterable` for the enum. + add_impls_for_plain_enum = variants.iter().all(|var| var.ty.is_none()); + let unit = syn::Type::Tuple(syn::TypeTuple { paren_token: Default::default(), elems: Default::default(), @@ -186,8 +191,7 @@ pub(crate) fn derive_satstype(ty: &SatsType<'_>) -> TokenStream { [#(#variants),*] ) ) - // todo!() - } // syn::Data::Union(u) => return Err(syn::Error::new(u.union_token.span, "unions not supported")), + } }; let mut sats_generics = ty.generics.clone(); @@ -205,7 +209,43 @@ pub(crate) fn derive_satstype(ty: &SatsType<'_>) -> TokenStream { } let (_, typeid_ty_generics, _) = typeid_generics.split_for_impl(); + let impl_plain_enum_extras = if add_impls_for_plain_enum { + // These will mostly be empty as lifetime and type parameters must be constrained + // but const parameters don't require constraining. + let mut generics = ty.generics.clone(); + add_type_bounds(&mut generics, "e!(#krate::FilterableValue)); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + // Assume that the type is `Copy`, as most all-unit enums will be. + let filterable_impl = quote! { + #[automatically_derived] + impl #impl_generics #krate::FilterableValue for #name #ty_generics #where_clause { + type Column = #name #ty_generics; + } + #[automatically_derived] + impl #impl_generics #krate::FilterableValue for &#name #ty_generics #where_clause { + type Column = #name #ty_generics; + } + }; + + let mut generics = ty.generics.clone(); + add_type_bounds(&mut generics, "e!(#krate::DirectIndexKey)); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let dik_impl = quote! { + #[automatically_derived] + impl #impl_generics #krate::DirectIndexKey for #name #ty_generics #where_clause {} + }; + + Some(quote! { + #filterable_impl + #dik_impl + }) + } else { + None + }; + quote! { + #impl_plain_enum_extras + #[automatically_derived] impl #impl_generics #krate::SpacetimeType for #name #ty_generics #where_clause { fn make_type(__typespace: &mut S) -> #krate::sats::AlgebraicType { diff --git a/crates/bindings-macro/src/table.rs b/crates/bindings-macro/src/table.rs index bda47031bba..3da4bee2e67 100644 --- a/crates/bindings-macro/src/table.rs +++ b/crates/bindings-macro/src/table.rs @@ -381,7 +381,7 @@ impl ValidatedIndex<'_> { let col_ty = col.ty; let typeck = quote_spanned!(col_ty.span()=> const _: () = { - spacetimedb::rt::assert_column_type_valid_for_direct_index::<#col_ty>(); + spacetimedb::spacetimedb_lib::assert_column_type_valid_for_direct_index::<#col_ty>(); }; ); (slice::from_ref(col), Some(typeck)) diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index bd313587cbc..4f91701c581 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -139,7 +139,7 @@ pub use spacetimedb_bindings_macro::client_visibility_filter; /// for popular_user in by_popularity.filter((100, "a"..)) { /// log::debug!("Popular user whose name starts with 'a': {:?}", popular_user); /// } -/// +/// /// // For every `#[unique]` or `#[primary_key]` field, /// // the table has an extra method that allows getting a /// // corresponding `spacetimedb::UniqueColumn`. @@ -359,7 +359,7 @@ pub use spacetimedb_bindings_macro::client_visibility_filter; /// ```ignore /// ctx.db.cities().latitude() /// ``` -/// +/// /// # Generated code /// /// For each `[table(name = {name})]` annotation on a type `{T}`, generates a struct @@ -402,7 +402,7 @@ pub use spacetimedb_bindings_macro::client_visibility_filter; /// impl {name}Handle { /// // For each `#[unique]` or `#[primary_key]` field `{field}` of type `{F}`: /// fn {field}(&self) -> UniqueColumn<_, {F}, _> { /* ... */ }; -/// +/// /// // For each named index `{index}` on fields of type `{(F1, ..., FN)}`: /// fn {index}(&self) -> RangedIndex<_, {(F1, ..., FN)}, _>; /// } diff --git a/crates/bindings/src/rt.rs b/crates/bindings/src/rt.rs index 5b2ec0ea1dc..9c3507b6583 100644 --- a/crates/bindings/src/rt.rs +++ b/crates/bindings/src/rt.rs @@ -346,26 +346,6 @@ pub fn register_table() { }) } -mod sealed_direct_index { - pub trait Sealed {} -} -#[diagnostic::on_unimplemented( - message = "column type must be a one of: `u8`, `u16`, `u32`, or `u64`", - label = "should be `u8`, `u16`, `u32`, or `u64`, not `{Self}`" -)] -pub trait DirectIndexKey: sealed_direct_index::Sealed {} -impl sealed_direct_index::Sealed for u8 {} -impl DirectIndexKey for u8 {} -impl sealed_direct_index::Sealed for u16 {} -impl DirectIndexKey for u16 {} -impl sealed_direct_index::Sealed for u32 {} -impl DirectIndexKey for u32 {} -impl sealed_direct_index::Sealed for u64 {} -impl DirectIndexKey for u64 {} - -/// Assert that `T` is a valid column to use direct index on. -pub const fn assert_column_type_valid_for_direct_index() {} - impl From> for RawIndexAlgorithm { fn from(algo: IndexAlgo<'_>) -> RawIndexAlgorithm { match algo { diff --git a/crates/bindings/src/table.rs b/crates/bindings/src/table.rs index e05501c8500..57436c1d937 100644 --- a/crates/bindings/src/table.rs +++ b/crates/bindings/src/table.rs @@ -1,17 +1,13 @@ -use std::borrow::Borrow; -use std::convert::Infallible; -use std::marker::PhantomData; -use std::{fmt, ops}; - +use crate::{bsatn, sys, DeserializeOwned, IterBuf, Serialize, SpacetimeType, TableId}; +use core::borrow::Borrow; +use core::convert::Infallible; +use core::fmt; +use core::marker::PhantomData; use spacetimedb_lib::buffer::{BufReader, Cursor, DecodeError}; -use spacetimedb_lib::sats::{i256, u256}; - pub use spacetimedb_lib::db::raw_def::v9::TableAccess; -use spacetimedb_lib::Hash; +use spacetimedb_lib::{FilterableValue, IndexScanRangeBoundsTerminator}; pub use spacetimedb_primitives::{ColId, IndexId}; -use crate::{bsatn, sys, ConnectionId, DeserializeOwned, Identity, IterBuf, Serialize, SpacetimeType, TableId}; - /// Implemented for every `TableHandle` struct generated by the [`table`](macro@crate::table) macro. /// Contains methods that are present for every table, regardless of what unique constraints /// and indexes are present. @@ -479,7 +475,7 @@ impl RangedIndex { /// /// fn demo(ctx: &ReducerContext) { /// let by_dogs_and_name: RangedIndex<_, (u64, String), _> = ctx.db.user().dogs_and_name(); - /// + /// /// // Find user with exactly 25 dogs. /// for user in by_dogs_and_name.filter(25u64) { // The `u64` is required, see below. /// /* ... */ @@ -566,7 +562,7 @@ impl RangedIndex { /// /// fn demo(ctx: &ReducerContext) { /// let by_dogs_and_name: RangedIndex<_, (u64, String), _> = ctx.db.user().dogs_and_name(); - /// + /// /// // Delete users with exactly 25 dogs. /// by_dogs_and_name.delete(25u64); // The `u64` is required, see below. /// @@ -622,85 +618,6 @@ impl RangedIndex { } } -mod private_filtrable_value { - pub trait Sealed {} -} - -/// Types which can appear as an argument to an index filtering operation -/// for a column of type `Column`. -/// -/// Types which can appear specifically as a terminating bound in a BTree index, -/// which may be a range, instead use [`IndexScanRangeBoundsTerminator`]. -/// -/// General rules for implementors of this type: -/// - It should only be implemented for types that have -/// simple-to-implement consistent total equality and ordering -/// on all languages SpacetimeDB supports in both client and module SDKs. -/// This means that user-defined compound types other than C-style enums, -/// and arrays thereof, -/// should not implement it, as C# and TypeScript use reference equality for those types. -/// - It should only be implemented for owned values if those values are `Copy`. -/// Otherwise it should only be implemented for references. -/// This is so that rustc and IDEs will recommend rewriting `x` to `&x` rather than `x.clone()`. -/// - `Arg: FilterableValue` -/// for any pair of types `(Arg, Col)` which meet the above criteria -/// is desirable if `Arg` and `Col` have the same BSATN layout. -/// E.g. `&str: FilterableValue` is desirable. -#[diagnostic::on_unimplemented( - message = "`{Self}` cannot appear as an argument to an index filtering operation", - label = "should be an integer type, `bool`, `String`, `&str`, `Identity`, `ConnectionId`, or `Hash`, not `{Self}`", - note = "The allowed set of types are limited to integers, bool, strings, `Identity`, `ConnectionId`, and `Hash`" -)] -pub trait FilterableValue: Serialize + private_filtrable_value::Sealed { - type Column; -} - -macro_rules! impl_filterable_value { - (@one $arg:ty => $col:ty) => { - impl private_filtrable_value::Sealed for $arg {} - impl FilterableValue for $arg { - type Column = $col; - } - }; - (@one $arg:ty: Copy) => { - impl_filterable_value!(@one $arg => $arg); - impl_filterable_value!(@one &$arg => $arg); - }; - (@one $arg:ty) => { - impl_filterable_value!(@one &$arg => $arg); - }; - ($($arg:ty $(: $copy:ident)? $(=> $col:ty)?),* $(,)?) => { - $(impl_filterable_value!(@one $arg $(: $copy)? $(=> $col)?);)* - }; -} - -impl_filterable_value! { - u8: Copy, - u16: Copy, - u32: Copy, - u64: Copy, - u128: Copy, - u256: Copy, - i8: Copy, - i16: Copy, - i32: Copy, - i64: Copy, - i128: Copy, - i256: Copy, - bool: Copy, - String, - &str => String, - Identity: Copy, - ConnectionId: Copy, - Hash: Copy, - - // Some day we will likely also want to support `Vec` and `[u8]`, - // as they have trivial portable equality and ordering, - // but @RReverser's proposed filtering rules do not include them. - // Vec, - // &[u8] => Vec, -} - /// Trait used for overloading methods on [`RangedIndex`]. /// See [`RangedIndex`] for more information. pub trait IndexScanRangeBounds { @@ -874,62 +791,6 @@ impl_index_scan_range_bounds!( (ArgA, ArgB, ArgC, ArgD, ArgE, ArgF) ); -pub enum TermBound { - Single(ops::Bound), - Range(ops::Bound, ops::Bound), -} -impl TermBound<&Bound> { - #[inline] - /// If `self` is [`TermBound::Range`], returns the `rend_idx` value for [`BTreeScanArgs`], - /// i.e. the index in `buf` of the first byte in the end range - fn serialize_into(&self, buf: &mut Vec) -> Option { - let (start, end) = match self { - TermBound::Single(elem) => (elem, None), - TermBound::Range(start, end) => (start, Some(end)), - }; - bsatn::to_writer(buf, start).unwrap(); - end.map(|end| { - let rend_idx = buf.len(); - bsatn::to_writer(buf, end).unwrap(); - rend_idx - }) - } -} -pub trait IndexScanRangeBoundsTerminator { - type Arg; - fn bounds(&self) -> TermBound<&Self::Arg>; -} - -impl> IndexScanRangeBoundsTerminator for Arg { - type Arg = Arg; - fn bounds(&self) -> TermBound<&Arg> { - TermBound::Single(ops::Bound::Included(self)) - } -} - -macro_rules! impl_terminator { - ($($range:ty),* $(,)?) => { - $(impl IndexScanRangeBoundsTerminator for $range { - type Arg = T; - fn bounds(&self) -> TermBound<&T> { - TermBound::Range( - ops::RangeBounds::start_bound(self), - ops::RangeBounds::end_bound(self), - ) - } - })* - }; -} - -impl_terminator!( - ops::Range, - ops::RangeFrom, - ops::RangeInclusive, - ops::RangeTo, - ops::RangeToInclusive, - (ops::Bound, ops::Bound), -); - // Single-column indexes // impl IndexScanRangeBounds<(T,)> for Range {} // impl IndexScanRangeBounds<(T,)> for T {} diff --git a/crates/bindings/tests/ui/tables.rs b/crates/bindings/tests/ui/tables.rs index 41c1268bf2d..ce63cc27461 100644 --- a/crates/bindings/tests/ui/tables.rs +++ b/crates/bindings/tests/ui/tables.rs @@ -14,7 +14,9 @@ struct TypeParam { struct ConstParam {} #[derive(spacetimedb::SpacetimeType)] -enum Alpha { Beta, Gamma } +struct Alpha { + beta: u8, +} #[spacetimedb::table(name = delta)] struct Delta { @@ -27,8 +29,8 @@ struct Delta { #[spacetimedb::reducer] fn bad_filter_on_index(ctx: &spacetimedb::ReducerContext) { - ctx.db.delta().compound_a().find(Alpha::Beta); - ctx.db.delta().compound_b().filter(Alpha::Gamma); + ctx.db.delta().compound_a().find(Alpha { beta: 0 }); + ctx.db.delta().compound_b().filter(Alpha { beta: 1 }); } fn main() {} diff --git a/crates/bindings/tests/ui/tables.stderr b/crates/bindings/tests/ui/tables.stderr index b93b3a7e2d3..7ff241e3dee 100644 --- a/crates/bindings/tests/ui/tables.stderr +++ b/crates/bindings/tests/ui/tables.stderr @@ -118,9 +118,9 @@ note: required by a bound in `spacetimedb::spacetimedb_lib::ser::SerializeNamedP | ^^^^^^^^^ required by this bound in `SerializeNamedProduct::serialize_element` error[E0277]: `&'a Alpha` cannot appear as an argument to an index filtering operation - --> tests/ui/tables.rs:30:33 + --> tests/ui/tables.rs:32:33 | -30 | ctx.db.delta().compound_a().find(Alpha::Beta); +32 | ctx.db.delta().compound_a().find(Alpha { beta: 0 }); | ^^^^ should be an integer type, `bool`, `String`, `&str`, `Identity`, `ConnectionId`, or `Hash`, not `&'a Alpha` | = help: the trait `for<'a> FilterableValue` is not implemented for `&'a Alpha` @@ -128,12 +128,12 @@ error[E0277]: `&'a Alpha` cannot appear as an argument to an index filtering ope = help: the following other types implement trait `FilterableValue`: &ConnectionId &Identity + &Lifecycle + &RawMiscModuleExportV9 + &TableAccess + &TableType &bool ðnum::int::I256 - ðnum::uint::U256 - &i128 - &i16 - &i32 and $N others note: required by a bound in `UniqueColumn::::ColType, Col>::find` --> src/table.rs @@ -145,22 +145,22 @@ note: required by a bound in `UniqueColumn::::ColType, Col>::find` error[E0277]: the trait bound `Alpha: IndexScanRangeBounds<(Alpha,), SingleBound>` is not satisfied - --> tests/ui/tables.rs:31:40 + --> tests/ui/tables.rs:33:40 | -31 | ctx.db.delta().compound_b().filter(Alpha::Gamma); - | ------ ^^^^^^^^^^^^ the trait `FilterableValue` is not implemented for `Alpha` +33 | ctx.db.delta().compound_b().filter(Alpha { beta: 1 }); + | ------ ^^^^^^^^^^^^^^^^^ the trait `FilterableValue` is not implemented for `Alpha` | | | required by a bound introduced by this call | = help: the following other types implement trait `FilterableValue`: &ConnectionId &Identity + &Lifecycle + &RawMiscModuleExportV9 + &TableAccess + &TableType &bool ðnum::int::I256 - ðnum::uint::U256 - &i128 - &i16 - &i32 and $N others = note: required for `Alpha` to implement `IndexScanRangeBounds<(Alpha,), SingleBound>` note: required by a bound in `RangedIndex::::filter` diff --git a/crates/cli/src/subcommands/generate/rust.rs b/crates/cli/src/subcommands/generate/rust.rs index 7e21a575b30..18c7fd8fa12 100644 --- a/crates/cli/src/subcommands/generate/rust.rs +++ b/crates/cli/src/subcommands/generate/rust.rs @@ -77,7 +77,7 @@ impl Lang for Rust { AlgebraicTypeDef::Sum(sum) => { gen_and_print_imports(module, out, &sum.variants, &[typ.ty]); out.newline(); - define_enum_for_sum(module, out, &type_name, &sum.variants); + define_enum_for_sum(module, out, &type_name, &sum.variants, false); } AlgebraicTypeDef::PlainEnum(plain_enum) => { let variants = plain_enum @@ -86,7 +86,7 @@ impl Lang for Rust { .cloned() .map(|var| (var, AlgebraicTypeUse::Unit)) .collect::>(); - define_enum_for_sum(module, out, &type_name, &variants); + define_enum_for_sum(module, out, &type_name, &variants, true); } } out.newline(); @@ -220,7 +220,7 @@ pub(super) fn register_table(client_cache: &mut __sdk::ClientCache({table_name:?});"); - for (unique_field_ident, unique_field_type_use) in iter_unique_cols(&schema, product_def) { + for (unique_field_ident, unique_field_type_use) in iter_unique_cols(module.typespace_for_generate(), &schema, product_def) { let unique_field_name = unique_field_ident.deref().to_case(Case::Snake); let unique_field_type = type_name(module, unique_field_type_use); writeln!( @@ -276,7 +276,9 @@ pub(super) fn parse_table_update( " ); - for (unique_field_ident, unique_field_type_use) in iter_unique_cols(&schema, product_def) { + for (unique_field_ident, unique_field_type_use) in + iter_unique_cols(module.typespace_for_generate(), &schema, product_def) + { let unique_field_name = unique_field_ident.deref().to_case(Case::Snake); let unique_field_name_pascalcase = unique_field_name.to_case(Case::Pascal); @@ -651,14 +653,24 @@ fn print_enum_derives(output: &mut Indenter) { print_lines(output, ENUM_DERIVES); } +const PLAIN_ENUM_EXTRA_DERIVES: &[&str] = &["#[derive(Copy, Eq, Hash)]"]; + +fn print_plain_enum_extra_derives(output: &mut Indenter) { + print_lines(output, PLAIN_ENUM_EXTRA_DERIVES); +} + /// Generate a file which defines an `enum` corresponding to the `sum_type`. pub fn define_enum_for_sum( module: &ModuleDef, out: &mut Indenter, name: &str, variants: &[(Identifier, AlgebraicTypeUse)], + is_plain: bool, ) { print_enum_derives(out); + if is_plain { + print_plain_enum_extra_derives(out); + } write!(out, "pub enum {name} "); out.delimited_block( diff --git a/crates/cli/src/subcommands/generate/typescript.rs b/crates/cli/src/subcommands/generate/typescript.rs index d48432f32d8..bd07c36affc 100644 --- a/crates/cli/src/subcommands/generate/typescript.rs +++ b/crates/cli/src/subcommands/generate/typescript.rs @@ -154,7 +154,9 @@ export class {table_handle} {{ }); writeln!(out, "}}"); - for (unique_field_ident, unique_field_type_use) in iter_unique_cols(&schema, product_def) { + for (unique_field_ident, unique_field_type_use) in + iter_unique_cols(module.typespace_for_generate(), &schema, product_def) + { let unique_field_name = unique_field_ident.deref().to_case(Case::Snake); let unique_field_name_pascalcase = unique_field_name.to_case(Case::Pascal); diff --git a/crates/cli/src/subcommands/generate/util.rs b/crates/cli/src/subcommands/generate/util.rs index b22edf2404d..4b87b7d0a88 100644 --- a/crates/cli/src/subcommands/generate/util.rs +++ b/crates/cli/src/subcommands/generate/util.rs @@ -10,9 +10,12 @@ use convert_case::{Case, Casing}; use itertools::Itertools; use spacetimedb_lib::{db::raw_def::v9::Lifecycle, sats::AlgebraicTypeRef}; use spacetimedb_primitives::ColList; -use spacetimedb_schema::def::{IndexDef, TableDef, TypeDef}; use spacetimedb_schema::schema::TableSchema; use spacetimedb_schema::type_for_generate::ProductTypeDef; +use spacetimedb_schema::{ + def::{IndexDef, TableDef, TypeDef}, + type_for_generate::TypespaceForGenerate, +}; use spacetimedb_schema::{ def::{ModuleDef, ReducerDef}, identifier::Identifier, @@ -57,10 +60,14 @@ pub(super) fn type_ref_name(module: &ModuleDef, typeref: AlgebraicTypeRef) -> St collect_case(Case::Pascal, name.name_segments()) } -pub(super) fn is_type_filterable(ty: &AlgebraicTypeUse) -> bool { +pub(super) fn is_type_filterable(typespace: &TypespaceForGenerate, ty: &AlgebraicTypeUse) -> bool { match ty { AlgebraicTypeUse::Primitive(prim) => !matches!(prim, PrimitiveType::F32 | PrimitiveType::F64), AlgebraicTypeUse::String | AlgebraicTypeUse::Identity | AlgebraicTypeUse::ConnectionId => true, + // Sum types with all unit variants: + AlgebraicTypeUse::Never => true, + AlgebraicTypeUse::Option(inner) => matches!(&**inner, AlgebraicTypeUse::Unit), + AlgebraicTypeUse::Ref(r) => typespace[r].is_plain_enum(), _ => false, } } @@ -87,6 +94,7 @@ pub(super) fn iter_tables(module: &ModuleDef) -> impl Iterator } pub(super) fn iter_unique_cols<'a>( + typespace: &'a TypespaceForGenerate, schema: &'a TableSchema, product_def: &'a ProductTypeDef, ) -> impl Iterator + 'a { @@ -96,7 +104,7 @@ pub(super) fn iter_unique_cols<'a>( .has_unique() .then(|| { let res @ (_, ref ty) = &product_def.elements[field.col_pos.idx()]; - is_type_filterable(ty).then_some(res) + is_type_filterable(typespace, ty).then_some(res) }) .flatten() }) diff --git a/crates/cli/tests/snapshots/codegen__codegen_rust.snap b/crates/cli/tests/snapshots/codegen__codegen_rust.snap index 606b397499d..b29c92a04b7 100644 --- a/crates/cli/tests/snapshots/codegen__codegen_rust.snap +++ b/crates/cli/tests/snapshots/codegen__codegen_rust.snap @@ -2270,6 +2270,7 @@ use spacetimedb_sdk::__codegen::{ #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] +#[derive(Copy, Eq, Hash)] pub enum NamespaceTestC { Foo, diff --git a/crates/lib/src/direct_index_key.rs b/crates/lib/src/direct_index_key.rs new file mode 100644 index 00000000000..8ab87661b30 --- /dev/null +++ b/crates/lib/src/direct_index_key.rs @@ -0,0 +1,12 @@ +#[diagnostic::on_unimplemented( + message = "column type must be a one of: `u8`, `u16`, `u32`, `u64`, or plain `enum`", + label = "should be `u8`, `u16`, `u32`, `u64`, or plain `enum`, not `{Self}`" +)] +pub trait DirectIndexKey {} +impl DirectIndexKey for u8 {} +impl DirectIndexKey for u16 {} +impl DirectIndexKey for u32 {} +impl DirectIndexKey for u64 {} + +/// Assert that `T` is a valid column to use direct index on. +pub const fn assert_column_type_valid_for_direct_index() {} diff --git a/crates/lib/src/filterable_value.rs b/crates/lib/src/filterable_value.rs new file mode 100644 index 00000000000..6f29059b5b0 --- /dev/null +++ b/crates/lib/src/filterable_value.rs @@ -0,0 +1,138 @@ +use crate::{ConnectionId, Identity}; +use core::ops; +use spacetimedb_sats::bsatn; +use spacetimedb_sats::{hash::Hash, i256, u256, Serialize}; + +/// Types which can appear as an argument to an index filtering operation +/// for a column of type `Column`. +/// +/// Types which can appear specifically as a terminating bound in a BTree index, +/// which may be a range, instead use [`IndexScanRangeBoundsTerminator`]. +/// +/// General rules for implementors of this type: +/// - It should only be implemented for types that have +/// simple-to-implement consistent total equality and ordering +/// on all languages SpacetimeDB supports in both client and module SDKs. +/// This means that user-defined compound types other than C-style enums, +/// and arrays thereof, +/// should not implement it, as C# and TypeScript use reference equality for those types. +/// - It should only be implemented for owned values if those values are `Copy`. +/// Otherwise it should only be implemented for references. +/// This is so that rustc and IDEs will recommend rewriting `x` to `&x` rather than `x.clone()`. +/// - `Arg: FilterableValue` +/// for any pair of types `(Arg, Col)` which meet the above criteria +/// is desirable if `Arg` and `Col` have the same BSATN layout. +/// E.g. `&str: FilterableValue` is desirable. +#[diagnostic::on_unimplemented( + message = "`{Self}` cannot appear as an argument to an index filtering operation", + label = "should be an integer type, `bool`, `String`, `&str`, `Identity`, `ConnectionId`, or `Hash`, not `{Self}`", + note = "The allowed set of types are limited to integers, bool, strings, `Identity`, `ConnectionId`, and `Hash`" +)] +pub trait FilterableValue: Serialize { + type Column; +} + +macro_rules! impl_filterable_value { + (@one $arg:ty => $col:ty) => { + impl FilterableValue for $arg { + type Column = $col; + } + }; + (@one $arg:ty: Copy) => { + impl_filterable_value!(@one $arg => $arg); + impl_filterable_value!(@one &$arg => $arg); + }; + (@one $arg:ty) => { + impl_filterable_value!(@one &$arg => $arg); + }; + ($($arg:ty $(: $copy:ident)? $(=> $col:ty)?),* $(,)?) => { + $(impl_filterable_value!(@one $arg $(: $copy)? $(=> $col)?);)* + }; +} + +impl_filterable_value! { + u8: Copy, + u16: Copy, + u32: Copy, + u64: Copy, + u128: Copy, + u256: Copy, + + i8: Copy, + i16: Copy, + i32: Copy, + i64: Copy, + i128: Copy, + i256: Copy, + + bool: Copy, + + String, + &str => String, + + Identity: Copy, + ConnectionId: Copy, + Hash: Copy, + + // Some day we will likely also want to support `Vec` and `[u8]`, + // as they have trivial portable equality and ordering, + // but @RReverser's proposed filtering rules do not include them. + // Vec, + // &[u8] => Vec, +} + +pub enum TermBound { + Single(ops::Bound), + Range(ops::Bound, ops::Bound), +} +impl TermBound<&Bound> { + #[inline] + /// If `self` is [`TermBound::Range`], returns the `rend_idx` value for `IndexScanRangeArgs`, + /// i.e. the index in `buf` of the first byte in the end range + pub fn serialize_into(&self, buf: &mut Vec) -> Option { + let (start, end) = match self { + TermBound::Single(elem) => (elem, None), + TermBound::Range(start, end) => (start, Some(end)), + }; + bsatn::to_writer(buf, start).unwrap(); + end.map(|end| { + let rend_idx = buf.len(); + bsatn::to_writer(buf, end).unwrap(); + rend_idx + }) + } +} +pub trait IndexScanRangeBoundsTerminator { + type Arg; + fn bounds(&self) -> TermBound<&Self::Arg>; +} + +impl> IndexScanRangeBoundsTerminator for Arg { + type Arg = Arg; + fn bounds(&self) -> TermBound<&Arg> { + TermBound::Single(ops::Bound::Included(self)) + } +} + +macro_rules! impl_terminator { + ($($range:ty),* $(,)?) => { + $(impl IndexScanRangeBoundsTerminator for $range { + type Arg = T; + fn bounds(&self) -> TermBound<&T> { + TermBound::Range( + ops::RangeBounds::start_bound(self), + ops::RangeBounds::end_bound(self), + ) + } + })* + }; +} + +impl_terminator!( + ops::Range, + ops::RangeFrom, + ops::RangeInclusive, + ops::RangeTo, + ops::RangeToInclusive, + (ops::Bound, ops::Bound), +); diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 44bb703b419..f1963db6cdb 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -8,7 +8,9 @@ use std::collections::{btree_map, BTreeMap}; pub mod connection_id; pub mod db; +mod direct_index_key; pub mod error; +mod filterable_value; pub mod identity; pub mod metrics; pub mod operator; @@ -26,6 +28,8 @@ pub mod type_value { } pub use connection_id::ConnectionId; +pub use direct_index_key::{assert_column_type_valid_for_direct_index, DirectIndexKey}; +pub use filterable_value::{FilterableValue, IndexScanRangeBoundsTerminator, TermBound}; pub use identity::Identity; pub use scheduler::ScheduleAt; pub use spacetimedb_sats::hash::{self, hash_bytes, Hash}; diff --git a/crates/sats/src/algebraic_value.rs b/crates/sats/src/algebraic_value.rs index 863a3483e34..2666765c6d2 100644 --- a/crates/sats/src/algebraic_value.rs +++ b/crates/sats/src/algebraic_value.rs @@ -178,15 +178,13 @@ impl AlgebraicValue { /// Returns an [`AlgebraicValue`] representing a sum value with `tag` and `value`. pub fn sum(tag: u8, value: Self) -> Self { - let value = Box::new(value); - Self::Sum(SumValue { tag, value }) + Self::Sum(SumValue::new(tag, value)) } /// Returns an [`AlgebraicValue`] representing a sum value with `tag` and empty [AlgebraicValue::product], that is /// valid for simple enums without payload. pub fn enum_simple(tag: u8) -> Self { - let value = Box::new(AlgebraicValue::product(vec![])); - Self::Sum(SumValue { tag, value }) + Self::Sum(SumValue::new_simple(tag)) } /// Returns an [`AlgebraicValue`] representing a product value with the given `elements`. diff --git a/crates/sats/src/convert.rs b/crates/sats/src/convert.rs index 422a6e1ac5f..dfd4a8dbe7e 100644 --- a/crates/sats/src/convert.rs +++ b/crates/sats/src/convert.rs @@ -1,3 +1,4 @@ +use crate::sum_value::SumTag; use crate::{i256, u256}; use crate::{AlgebraicType, AlgebraicValue, ProductType, ProductValue}; use spacetimedb_primitives::{ColId, ConstraintId, IndexId, ScheduleId, SequenceId, TableId}; @@ -25,6 +26,12 @@ impl From for ProductType { } } +impl From<()> for AlgebraicValue { + fn from((): ()) -> Self { + AlgebraicValue::unit() + } +} + macro_rules! built_in_into { ($native:ty, $kind:ident) => { impl From<$native> for AlgebraicValue { @@ -45,6 +52,7 @@ built_in_into!(&str, String); built_in_into!(String, String); built_in_into!(&[u8], Bytes); built_in_into!(Box<[u8]>, Bytes); +built_in_into!(SumTag, Sum); macro_rules! system_id { ($name:ident) => { diff --git a/crates/sats/src/sum_value.rs b/crates/sats/src/sum_value.rs index 00c68bb2370..c0fe84e1b54 100644 --- a/crates/sats/src/sum_value.rs +++ b/crates/sats/src/sum_value.rs @@ -14,3 +14,27 @@ pub struct SumValue { impl crate::Value for SumValue { type Type = SumType; } + +impl SumValue { + /// Returns a new `SumValue` with the given `tag` and `value`. + pub fn new(tag: u8, value: impl Into) -> Self { + let value = Box::from(value.into()); + Self { tag, value } + } + + /// Returns a new `SumValue` with the given `tag` and unit value. + pub fn new_simple(tag: u8) -> Self { + Self::new(tag, ()) + } +} + +/// The tag of a `SumValue`. +/// Can be used to read out the tag of a sum value without reading the payload. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct SumTag(pub u8); + +impl From for SumValue { + fn from(SumTag(tag): SumTag) -> Self { + SumValue::new_simple(tag) + } +} diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index 712b181eddf..c3646c9fc52 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -625,9 +625,16 @@ impl TableValidator<'_, '_> { let field = &self.product_type.elements[column.idx()]; let ty = &field.algebraic_type; use AlgebraicType::*; - if let U8 | U16 | U32 | U64 = ty { - } else { - return Err(ValidationError::DirectIndexOnNonUnsignedInt { + let is_bad_type = match ty { + U8 | U16 | U32 | U64 => false, + Ref(r) => self.module_validator.typespace[*r] + .as_sum() + .is_none_or(|s| !s.is_simple_enum()), + Sum(sum) if sum.is_simple_enum() => false, + _ => true, + }; + if is_bad_type { + return Err(ValidationError::DirectIndexOnBadType { index: name.clone(), column: field.name.clone().unwrap_or_else(|| column.idx().to_string().into()), ty: ty.clone().into(), @@ -1440,7 +1447,7 @@ mod tests { .finish(); let result: Result = builder.finish().try_into(); - expect_error_matching!(result, ValidationError::DirectIndexOnNonUnsignedInt { index, .. } => { + expect_error_matching!(result, ValidationError::DirectIndexOnBadType { index, .. } => { &index[..] == "Bananas_b_idx_direct" }); } diff --git a/crates/schema/src/error.rs b/crates/schema/src/error.rs index fda129affbb..c5db5873b19 100644 --- a/crates/schema/src/error.rs +++ b/crates/schema/src/error.rs @@ -62,7 +62,7 @@ pub enum ValidationError { #[error("No index found to support unique constraint `{constraint}` for columns `{columns:?}`")] UniqueConstraintWithoutIndex { constraint: Box, columns: ColSet }, #[error("Direct index does not support type `{ty}` in column `{column}` in index `{index}`")] - DirectIndexOnNonUnsignedInt { + DirectIndexOnBadType { index: RawIdentifier, column: RawIdentifier, ty: PrettyAlgebraicType, diff --git a/crates/sdk/tests/test-client/src/main.rs b/crates/sdk/tests/test-client/src/main.rs index 93c453b1739..dc57ac251f9 100644 --- a/crates/sdk/tests/test-client/src/main.rs +++ b/crates/sdk/tests/test-client/src/main.rs @@ -9,6 +9,7 @@ use std::sync::{Arc, Barrier, Mutex}; use module_bindings::*; use rand::RngCore; +use spacetimedb_sdk::TableWithPrimaryKey; use spacetimedb_sdk::{ credentials, i256, u256, unstable::CallReducerFlags, Compression, ConnectionId, DbConnectionBuilder, DbContext, Event, Identity, ReducerEvent, Status, SubscriptionHandle, Table, TimeDuration, Timestamp, @@ -126,6 +127,8 @@ fn main() { "two-different-compression-algos" => exec_two_different_compression_algos(), "test-parameterized-subscription" => test_parameterized_subscription(), "test-rls-subscription" => test_rls_subscription(), + "pk-simple-enum" => exec_pk_simple_enum(), + "indexed-simple-enum" => exec_indexed_simple_enum(), _ => panic!("Unknown test: {}", test), } } @@ -2358,3 +2361,51 @@ fn test_rls_subscription() { ); ctr_for_test.wait_for_all(); } + +fn exec_pk_simple_enum() { + let test_counter: Arc = TestCounter::new(); + let mut updated = Some(test_counter.add_test("updated")); + connect_then(&test_counter, move |ctx| { + subscribe_these_then(ctx, &["SELECT * FROM pk_simple_enum"], move |ctx| { + let data1 = 42; + let data2 = 24; + let a = SimpleEnum::Two; + ctx.db.pk_simple_enum().on_update(move |_, old, new| { + assert_eq!(old.data, data1); + assert_eq!(new.data, data2); + assert_eq!(old.a, a); + assert_eq!(new.a, a); + put_result(&mut updated, Ok(())); + }); + ctx.db.pk_simple_enum().on_insert(move |ctx, row| { + assert_eq!(row.data, data1); + assert_eq!(row.a, a); + ctx.reducers().update_pk_simple_enum(a, data2).unwrap(); + }); + ctx.db.pk_simple_enum().on_delete(|_, _| unreachable!()); + ctx.reducers().insert_pk_simple_enum(a, data1).unwrap(); + }); + }); + test_counter.wait_for_all(); +} + +fn exec_indexed_simple_enum() { + let test_counter: Arc = TestCounter::new(); + let mut updated = Some(test_counter.add_test("updated")); + connect_then(&test_counter, move |ctx| { + subscribe_these_then(ctx, &["SELECT * FROM indexed_simple_enum"], move |ctx| { + let a1 = SimpleEnum::Two; + let a2 = SimpleEnum::One; + ctx.db.indexed_simple_enum().on_insert(move |ctx, row| match &row.n { + SimpleEnum::Two => ctx.reducers().update_indexed_simple_enum(a1, a2).unwrap(), + SimpleEnum::One => { + assert_eq!(row.n, a2); + put_result(&mut updated, Ok(())); + } + SimpleEnum::Zero => unreachable!(), + }); + ctx.reducers().insert_into_indexed_simple_enum(a1).unwrap(); + }); + }); + test_counter.wait_for_all(); +} diff --git a/crates/sdk/tests/test-client/src/module_bindings/indexed_simple_enum_table.rs b/crates/sdk/tests/test-client/src/module_bindings/indexed_simple_enum_table.rs new file mode 100644 index 00000000000..bec832af943 --- /dev/null +++ b/crates/sdk/tests/test-client/src/module_bindings/indexed_simple_enum_table.rs @@ -0,0 +1,96 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::indexed_simple_enum_type::IndexedSimpleEnum; +use super::simple_enum_type::SimpleEnum; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `indexed_simple_enum`. +/// +/// Obtain a handle from the [`IndexedSimpleEnumTableAccess::indexed_simple_enum`] method on [`super::RemoteTables`], +/// like `ctx.db.indexed_simple_enum()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.indexed_simple_enum().on_insert(...)`. +pub struct IndexedSimpleEnumTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `indexed_simple_enum`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait IndexedSimpleEnumTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`IndexedSimpleEnumTableHandle`], which mediates access to the table `indexed_simple_enum`. + fn indexed_simple_enum(&self) -> IndexedSimpleEnumTableHandle<'_>; +} + +impl IndexedSimpleEnumTableAccess for super::RemoteTables { + fn indexed_simple_enum(&self) -> IndexedSimpleEnumTableHandle<'_> { + IndexedSimpleEnumTableHandle { + imp: self.imp.get_table::("indexed_simple_enum"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct IndexedSimpleEnumInsertCallbackId(__sdk::CallbackId); +pub struct IndexedSimpleEnumDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for IndexedSimpleEnumTableHandle<'ctx> { + type Row = IndexedSimpleEnum; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = IndexedSimpleEnumInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> IndexedSimpleEnumInsertCallbackId { + IndexedSimpleEnumInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: IndexedSimpleEnumInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = IndexedSimpleEnumDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> IndexedSimpleEnumDeleteCallbackId { + IndexedSimpleEnumDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: IndexedSimpleEnumDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("indexed_simple_enum"); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::TableUpdate<__ws::BsatnFormat>, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} diff --git a/crates/sdk/tests/test-client/src/module_bindings/indexed_simple_enum_type.rs b/crates/sdk/tests/test-client/src/module_bindings/indexed_simple_enum_type.rs new file mode 100644 index 00000000000..cd4229db3b8 --- /dev/null +++ b/crates/sdk/tests/test-client/src/module_bindings/indexed_simple_enum_type.rs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::simple_enum_type::SimpleEnum; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct IndexedSimpleEnum { + pub n: SimpleEnum, +} + +impl __sdk::InModule for IndexedSimpleEnum { + type Module = super::RemoteModule; +} diff --git a/crates/sdk/tests/test-client/src/module_bindings/insert_into_indexed_simple_enum_reducer.rs b/crates/sdk/tests/test-client/src/module_bindings/insert_into_indexed_simple_enum_reducer.rs new file mode 100644 index 00000000000..da7d8dbd37a --- /dev/null +++ b/crates/sdk/tests/test-client/src/module_bindings/insert_into_indexed_simple_enum_reducer.rs @@ -0,0 +1,106 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::simple_enum_type::SimpleEnum; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub(super) struct InsertIntoIndexedSimpleEnumArgs { + pub n: SimpleEnum, +} + +impl From for super::Reducer { + fn from(args: InsertIntoIndexedSimpleEnumArgs) -> Self { + Self::InsertIntoIndexedSimpleEnum { n: args.n } + } +} + +impl __sdk::InModule for InsertIntoIndexedSimpleEnumArgs { + type Module = super::RemoteModule; +} + +pub struct InsertIntoIndexedSimpleEnumCallbackId(__sdk::CallbackId); + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `insert_into_indexed_simple_enum`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait insert_into_indexed_simple_enum { + /// Request that the remote module invoke the reducer `insert_into_indexed_simple_enum` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed by listening for [`Self::on_insert_into_indexed_simple_enum`] callbacks. + fn insert_into_indexed_simple_enum(&self, n: SimpleEnum) -> __sdk::Result<()>; + /// Register a callback to run whenever we are notified of an invocation of the reducer `insert_into_indexed_simple_enum`. + /// + /// Callbacks should inspect the [`__sdk::ReducerEvent`] contained in the [`super::ReducerEventContext`] + /// to determine the reducer's status. + /// + /// The returned [`InsertIntoIndexedSimpleEnumCallbackId`] can be passed to [`Self::remove_on_insert_into_indexed_simple_enum`] + /// to cancel the callback. + fn on_insert_into_indexed_simple_enum( + &self, + callback: impl FnMut(&super::ReducerEventContext, &SimpleEnum) + Send + 'static, + ) -> InsertIntoIndexedSimpleEnumCallbackId; + /// Cancel a callback previously registered by [`Self::on_insert_into_indexed_simple_enum`], + /// causing it not to run in the future. + fn remove_on_insert_into_indexed_simple_enum(&self, callback: InsertIntoIndexedSimpleEnumCallbackId); +} + +impl insert_into_indexed_simple_enum for super::RemoteReducers { + fn insert_into_indexed_simple_enum(&self, n: SimpleEnum) -> __sdk::Result<()> { + self.imp + .call_reducer("insert_into_indexed_simple_enum", InsertIntoIndexedSimpleEnumArgs { n }) + } + fn on_insert_into_indexed_simple_enum( + &self, + mut callback: impl FnMut(&super::ReducerEventContext, &SimpleEnum) + Send + 'static, + ) -> InsertIntoIndexedSimpleEnumCallbackId { + InsertIntoIndexedSimpleEnumCallbackId(self.imp.on_reducer( + "insert_into_indexed_simple_enum", + Box::new(move |ctx: &super::ReducerEventContext| { + let super::ReducerEventContext { + event: + __sdk::ReducerEvent { + reducer: super::Reducer::InsertIntoIndexedSimpleEnum { n }, + .. + }, + .. + } = ctx + else { + unreachable!() + }; + callback(ctx, n) + }), + )) + } + fn remove_on_insert_into_indexed_simple_enum(&self, callback: InsertIntoIndexedSimpleEnumCallbackId) { + self.imp + .remove_on_reducer("insert_into_indexed_simple_enum", callback.0) + } +} + +#[allow(non_camel_case_types)] +#[doc(hidden)] +/// Extension trait for setting the call-flags for the reducer `insert_into_indexed_simple_enum`. +/// +/// Implemented for [`super::SetReducerFlags`]. +/// +/// This type is currently unstable and may be removed without a major version bump. +pub trait set_flags_for_insert_into_indexed_simple_enum { + /// Set the call-reducer flags for the reducer `insert_into_indexed_simple_enum` to `flags`. + /// + /// This type is currently unstable and may be removed without a major version bump. + fn insert_into_indexed_simple_enum(&self, flags: __ws::CallReducerFlags); +} + +impl set_flags_for_insert_into_indexed_simple_enum for super::SetReducerFlags { + fn insert_into_indexed_simple_enum(&self, flags: __ws::CallReducerFlags) { + self.imp + .set_call_reducer_flags("insert_into_indexed_simple_enum", flags); + } +} diff --git a/crates/sdk/tests/test-client/src/module_bindings/insert_pk_simple_enum_reducer.rs b/crates/sdk/tests/test-client/src/module_bindings/insert_pk_simple_enum_reducer.rs new file mode 100644 index 00000000000..39828792c08 --- /dev/null +++ b/crates/sdk/tests/test-client/src/module_bindings/insert_pk_simple_enum_reducer.rs @@ -0,0 +1,108 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::simple_enum_type::SimpleEnum; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub(super) struct InsertPkSimpleEnumArgs { + pub a: SimpleEnum, + pub data: i32, +} + +impl From for super::Reducer { + fn from(args: InsertPkSimpleEnumArgs) -> Self { + Self::InsertPkSimpleEnum { + a: args.a, + data: args.data, + } + } +} + +impl __sdk::InModule for InsertPkSimpleEnumArgs { + type Module = super::RemoteModule; +} + +pub struct InsertPkSimpleEnumCallbackId(__sdk::CallbackId); + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `insert_pk_simple_enum`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait insert_pk_simple_enum { + /// Request that the remote module invoke the reducer `insert_pk_simple_enum` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed by listening for [`Self::on_insert_pk_simple_enum`] callbacks. + fn insert_pk_simple_enum(&self, a: SimpleEnum, data: i32) -> __sdk::Result<()>; + /// Register a callback to run whenever we are notified of an invocation of the reducer `insert_pk_simple_enum`. + /// + /// Callbacks should inspect the [`__sdk::ReducerEvent`] contained in the [`super::ReducerEventContext`] + /// to determine the reducer's status. + /// + /// The returned [`InsertPkSimpleEnumCallbackId`] can be passed to [`Self::remove_on_insert_pk_simple_enum`] + /// to cancel the callback. + fn on_insert_pk_simple_enum( + &self, + callback: impl FnMut(&super::ReducerEventContext, &SimpleEnum, &i32) + Send + 'static, + ) -> InsertPkSimpleEnumCallbackId; + /// Cancel a callback previously registered by [`Self::on_insert_pk_simple_enum`], + /// causing it not to run in the future. + fn remove_on_insert_pk_simple_enum(&self, callback: InsertPkSimpleEnumCallbackId); +} + +impl insert_pk_simple_enum for super::RemoteReducers { + fn insert_pk_simple_enum(&self, a: SimpleEnum, data: i32) -> __sdk::Result<()> { + self.imp + .call_reducer("insert_pk_simple_enum", InsertPkSimpleEnumArgs { a, data }) + } + fn on_insert_pk_simple_enum( + &self, + mut callback: impl FnMut(&super::ReducerEventContext, &SimpleEnum, &i32) + Send + 'static, + ) -> InsertPkSimpleEnumCallbackId { + InsertPkSimpleEnumCallbackId(self.imp.on_reducer( + "insert_pk_simple_enum", + Box::new(move |ctx: &super::ReducerEventContext| { + let super::ReducerEventContext { + event: + __sdk::ReducerEvent { + reducer: super::Reducer::InsertPkSimpleEnum { a, data }, + .. + }, + .. + } = ctx + else { + unreachable!() + }; + callback(ctx, a, data) + }), + )) + } + fn remove_on_insert_pk_simple_enum(&self, callback: InsertPkSimpleEnumCallbackId) { + self.imp.remove_on_reducer("insert_pk_simple_enum", callback.0) + } +} + +#[allow(non_camel_case_types)] +#[doc(hidden)] +/// Extension trait for setting the call-flags for the reducer `insert_pk_simple_enum`. +/// +/// Implemented for [`super::SetReducerFlags`]. +/// +/// This type is currently unstable and may be removed without a major version bump. +pub trait set_flags_for_insert_pk_simple_enum { + /// Set the call-reducer flags for the reducer `insert_pk_simple_enum` to `flags`. + /// + /// This type is currently unstable and may be removed without a major version bump. + fn insert_pk_simple_enum(&self, flags: __ws::CallReducerFlags); +} + +impl set_flags_for_insert_pk_simple_enum for super::SetReducerFlags { + fn insert_pk_simple_enum(&self, flags: __ws::CallReducerFlags) { + self.imp.set_call_reducer_flags("insert_pk_simple_enum", flags); + } +} diff --git a/crates/sdk/tests/test-client/src/module_bindings/mod.rs b/crates/sdk/tests/test-client/src/module_bindings/mod.rs index 3304d93cdc1..e475a82bf44 100644 --- a/crates/sdk/tests/test-client/src/module_bindings/mod.rs +++ b/crates/sdk/tests/test-client/src/module_bindings/mod.rs @@ -46,6 +46,8 @@ pub mod delete_unique_u_8_reducer; pub mod enum_with_payload_type; pub mod every_primitive_struct_type; pub mod every_vec_struct_type; +pub mod indexed_simple_enum_table; +pub mod indexed_simple_enum_type; pub mod indexed_table_2_table; pub mod indexed_table_2_type; pub mod indexed_table_table; @@ -60,6 +62,7 @@ pub mod insert_caller_unique_identity_reducer; pub mod insert_caller_vec_connection_id_reducer; pub mod insert_caller_vec_identity_reducer; pub mod insert_into_btree_u_32_reducer; +pub mod insert_into_indexed_simple_enum_reducer; pub mod insert_into_pk_btree_u_32_reducer; pub mod insert_large_table_reducer; pub mod insert_one_bool_reducer; @@ -102,6 +105,7 @@ pub mod insert_pk_i_32_reducer; pub mod insert_pk_i_64_reducer; pub mod insert_pk_i_8_reducer; pub mod insert_pk_identity_reducer; +pub mod insert_pk_simple_enum_reducer; pub mod insert_pk_string_reducer; pub mod insert_pk_u_128_reducer; pub mod insert_pk_u_16_reducer; @@ -238,6 +242,8 @@ pub mod pk_i_8_table; pub mod pk_i_8_type; pub mod pk_identity_table; pub mod pk_identity_type; +pub mod pk_simple_enum_table; +pub mod pk_simple_enum_type; pub mod pk_string_table; pub mod pk_string_type; pub mod pk_u_128_table; @@ -293,6 +299,7 @@ pub mod unique_u_64_type; pub mod unique_u_8_table; pub mod unique_u_8_type; pub mod unit_struct_type; +pub mod update_indexed_simple_enum_reducer; pub mod update_pk_bool_reducer; pub mod update_pk_connection_id_reducer; pub mod update_pk_i_128_reducer; @@ -302,6 +309,7 @@ pub mod update_pk_i_32_reducer; pub mod update_pk_i_64_reducer; pub mod update_pk_i_8_reducer; pub mod update_pk_identity_reducer; +pub mod update_pk_simple_enum_reducer; pub mod update_pk_string_reducer; pub mod update_pk_u_128_reducer; pub mod update_pk_u_16_reducer; @@ -448,6 +456,8 @@ pub use delete_unique_u_8_reducer::{delete_unique_u_8, set_flags_for_delete_uniq pub use enum_with_payload_type::EnumWithPayload; pub use every_primitive_struct_type::EveryPrimitiveStruct; pub use every_vec_struct_type::EveryVecStruct; +pub use indexed_simple_enum_table::*; +pub use indexed_simple_enum_type::IndexedSimpleEnum; pub use indexed_table_2_table::*; pub use indexed_table_2_type::IndexedTable2; pub use indexed_table_table::*; @@ -485,6 +495,10 @@ pub use insert_caller_vec_identity_reducer::{ pub use insert_into_btree_u_32_reducer::{ insert_into_btree_u_32, set_flags_for_insert_into_btree_u_32, InsertIntoBtreeU32CallbackId, }; +pub use insert_into_indexed_simple_enum_reducer::{ + insert_into_indexed_simple_enum, set_flags_for_insert_into_indexed_simple_enum, + InsertIntoIndexedSimpleEnumCallbackId, +}; pub use insert_into_pk_btree_u_32_reducer::{ insert_into_pk_btree_u_32, set_flags_for_insert_into_pk_btree_u_32, InsertIntoPkBtreeU32CallbackId, }; @@ -565,6 +579,9 @@ pub use insert_pk_i_8_reducer::{insert_pk_i_8, set_flags_for_insert_pk_i_8, Inse pub use insert_pk_identity_reducer::{ insert_pk_identity, set_flags_for_insert_pk_identity, InsertPkIdentityCallbackId, }; +pub use insert_pk_simple_enum_reducer::{ + insert_pk_simple_enum, set_flags_for_insert_pk_simple_enum, InsertPkSimpleEnumCallbackId, +}; pub use insert_pk_string_reducer::{insert_pk_string, set_flags_for_insert_pk_string, InsertPkStringCallbackId}; pub use insert_pk_u_128_reducer::{insert_pk_u_128, set_flags_for_insert_pk_u_128, InsertPkU128CallbackId}; pub use insert_pk_u_16_reducer::{insert_pk_u_16, set_flags_for_insert_pk_u_16, InsertPkU16CallbackId}; @@ -743,6 +760,8 @@ pub use pk_i_8_table::*; pub use pk_i_8_type::PkI8; pub use pk_identity_table::*; pub use pk_identity_type::PkIdentity; +pub use pk_simple_enum_table::*; +pub use pk_simple_enum_type::PkSimpleEnum; pub use pk_string_table::*; pub use pk_string_type::PkString; pub use pk_u_128_table::*; @@ -800,6 +819,9 @@ pub use unique_u_64_type::UniqueU64; pub use unique_u_8_table::*; pub use unique_u_8_type::UniqueU8; pub use unit_struct_type::UnitStruct; +pub use update_indexed_simple_enum_reducer::{ + set_flags_for_update_indexed_simple_enum, update_indexed_simple_enum, UpdateIndexedSimpleEnumCallbackId, +}; pub use update_pk_bool_reducer::{set_flags_for_update_pk_bool, update_pk_bool, UpdatePkBoolCallbackId}; pub use update_pk_connection_id_reducer::{ set_flags_for_update_pk_connection_id, update_pk_connection_id, UpdatePkConnectionIdCallbackId, @@ -813,6 +835,9 @@ pub use update_pk_i_8_reducer::{set_flags_for_update_pk_i_8, update_pk_i_8, Upda pub use update_pk_identity_reducer::{ set_flags_for_update_pk_identity, update_pk_identity, UpdatePkIdentityCallbackId, }; +pub use update_pk_simple_enum_reducer::{ + set_flags_for_update_pk_simple_enum, update_pk_simple_enum, UpdatePkSimpleEnumCallbackId, +}; pub use update_pk_string_reducer::{set_flags_for_update_pk_string, update_pk_string, UpdatePkStringCallbackId}; pub use update_pk_u_128_reducer::{set_flags_for_update_pk_u_128, update_pk_u_128, UpdatePkU128CallbackId}; pub use update_pk_u_16_reducer::{set_flags_for_update_pk_u_16, update_pk_u_16, UpdatePkU16CallbackId}; @@ -1064,6 +1089,9 @@ pub enum Reducer { InsertIntoBtreeU32 { rows: Vec, }, + InsertIntoIndexedSimpleEnum { + n: SimpleEnum, + }, InsertIntoPkBtreeU32 { pk_u_32: Vec, bt_u_32: Vec, @@ -1221,6 +1249,10 @@ pub enum Reducer { i: __sdk::Identity, data: i32, }, + InsertPkSimpleEnum { + a: SimpleEnum, + data: i32, + }, InsertPkString { s: String, data: i32, @@ -1412,6 +1444,10 @@ pub enum Reducer { SendScheduledMessage { arg: ScheduledTable, }, + UpdateIndexedSimpleEnum { + a: SimpleEnum, + b: SimpleEnum, + }, UpdatePkBool { b: bool, data: i32, @@ -1448,6 +1484,10 @@ pub enum Reducer { i: __sdk::Identity, data: i32, }, + UpdatePkSimpleEnum { + a: SimpleEnum, + data: i32, + }, UpdatePkString { s: String, data: i32, @@ -1599,6 +1639,7 @@ impl __sdk::Reducer for Reducer { Reducer::InsertCallerVecConnectionId => "insert_caller_vec_connection_id", Reducer::InsertCallerVecIdentity => "insert_caller_vec_identity", Reducer::InsertIntoBtreeU32 { .. } => "insert_into_btree_u32", + Reducer::InsertIntoIndexedSimpleEnum { .. } => "insert_into_indexed_simple_enum", Reducer::InsertIntoPkBtreeU32 { .. } => "insert_into_pk_btree_u32", Reducer::InsertLargeTable { .. } => "insert_large_table", Reducer::InsertOneBool { .. } => "insert_one_bool", @@ -1641,6 +1682,7 @@ impl __sdk::Reducer for Reducer { Reducer::InsertPkI64 { .. } => "insert_pk_i64", Reducer::InsertPkI8 { .. } => "insert_pk_i8", Reducer::InsertPkIdentity { .. } => "insert_pk_identity", + Reducer::InsertPkSimpleEnum { .. } => "insert_pk_simple_enum", Reducer::InsertPkString { .. } => "insert_pk_string", Reducer::InsertPkU128 { .. } => "insert_pk_u128", Reducer::InsertPkU16 { .. } => "insert_pk_u16", @@ -1696,6 +1738,7 @@ impl __sdk::Reducer for Reducer { Reducer::InsertVecUnitStruct { .. } => "insert_vec_unit_struct", Reducer::NoOpSucceeds => "no_op_succeeds", Reducer::SendScheduledMessage { .. } => "send_scheduled_message", + Reducer::UpdateIndexedSimpleEnum { .. } => "update_indexed_simple_enum", Reducer::UpdatePkBool { .. } => "update_pk_bool", Reducer::UpdatePkConnectionId { .. } => "update_pk_connection_id", Reducer::UpdatePkI128 { .. } => "update_pk_i128", @@ -1705,6 +1748,7 @@ impl __sdk::Reducer for Reducer { Reducer::UpdatePkI64 { .. } => "update_pk_i64", Reducer::UpdatePkI8 { .. } => "update_pk_i8", Reducer::UpdatePkIdentity { .. } => "update_pk_identity", + Reducer::UpdatePkSimpleEnum { .. } => "update_pk_simple_enum", Reducer::UpdatePkString { .. } => "update_pk_string", Reducer::UpdatePkU128 { .. } => "update_pk_u128", Reducer::UpdatePkU16 { .. } => "update_pk_u16", @@ -1986,6 +2030,10 @@ impl TryFrom<__ws::ReducerCallInfo<__ws::BsatnFormat>> for Reducer { insert_into_btree_u_32_reducer::InsertIntoBtreeU32Args, >("insert_into_btree_u32", &value.args)? .into()), + "insert_into_indexed_simple_enum" => Ok(__sdk::parse_reducer_args::< + insert_into_indexed_simple_enum_reducer::InsertIntoIndexedSimpleEnumArgs, + >("insert_into_indexed_simple_enum", &value.args)? + .into()), "insert_into_pk_btree_u32" => Ok(__sdk::parse_reducer_args::< insert_into_pk_btree_u_32_reducer::InsertIntoPkBtreeU32Args, >("insert_into_pk_btree_u32", &value.args)? @@ -2201,6 +2249,10 @@ impl TryFrom<__ws::ReducerCallInfo<__ws::BsatnFormat>> for Reducer { )? .into(), ), + "insert_pk_simple_enum" => Ok(__sdk::parse_reducer_args::< + insert_pk_simple_enum_reducer::InsertPkSimpleEnumArgs, + >("insert_pk_simple_enum", &value.args)? + .into()), "insert_pk_string" => Ok( __sdk::parse_reducer_args::( "insert_pk_string", @@ -2503,6 +2555,10 @@ impl TryFrom<__ws::ReducerCallInfo<__ws::BsatnFormat>> for Reducer { send_scheduled_message_reducer::SendScheduledMessageArgs, >("send_scheduled_message", &value.args)? .into()), + "update_indexed_simple_enum" => Ok(__sdk::parse_reducer_args::< + update_indexed_simple_enum_reducer::UpdateIndexedSimpleEnumArgs, + >("update_indexed_simple_enum", &value.args)? + .into()), "update_pk_bool" => Ok(__sdk::parse_reducer_args::( "update_pk_bool", &value.args, @@ -2549,6 +2605,10 @@ impl TryFrom<__ws::ReducerCallInfo<__ws::BsatnFormat>> for Reducer { )? .into(), ), + "update_pk_simple_enum" => Ok(__sdk::parse_reducer_args::< + update_pk_simple_enum_reducer::UpdatePkSimpleEnumArgs, + >("update_pk_simple_enum", &value.args)? + .into()), "update_pk_string" => Ok( __sdk::parse_reducer_args::( "update_pk_string", @@ -2706,6 +2766,7 @@ impl TryFrom<__ws::ReducerCallInfo<__ws::BsatnFormat>> for Reducer { #[doc(hidden)] pub struct DbUpdate { btree_u_32: __sdk::TableUpdate, + indexed_simple_enum: __sdk::TableUpdate, indexed_table: __sdk::TableUpdate, indexed_table_2: __sdk::TableUpdate, large_table: __sdk::TableUpdate, @@ -2749,6 +2810,7 @@ pub struct DbUpdate { pk_i_64: __sdk::TableUpdate, pk_i_8: __sdk::TableUpdate, pk_identity: __sdk::TableUpdate, + pk_simple_enum: __sdk::TableUpdate, pk_string: __sdk::TableUpdate, pk_u_128: __sdk::TableUpdate, pk_u_16: __sdk::TableUpdate, @@ -2810,6 +2872,9 @@ impl TryFrom<__ws::DatabaseUpdate<__ws::BsatnFormat>> for DbUpdate { for table_update in raw.tables { match &table_update.table_name[..] { "btree_u32" => db_update.btree_u_32 = btree_u_32_table::parse_table_update(table_update)?, + "indexed_simple_enum" => { + db_update.indexed_simple_enum = indexed_simple_enum_table::parse_table_update(table_update)? + } "indexed_table" => db_update.indexed_table = indexed_table_table::parse_table_update(table_update)?, "indexed_table_2" => { db_update.indexed_table_2 = indexed_table_2_table::parse_table_update(table_update)? @@ -2881,6 +2946,7 @@ impl TryFrom<__ws::DatabaseUpdate<__ws::BsatnFormat>> for DbUpdate { "pk_i64" => db_update.pk_i_64 = pk_i_64_table::parse_table_update(table_update)?, "pk_i8" => db_update.pk_i_8 = pk_i_8_table::parse_table_update(table_update)?, "pk_identity" => db_update.pk_identity = pk_identity_table::parse_table_update(table_update)?, + "pk_simple_enum" => db_update.pk_simple_enum = pk_simple_enum_table::parse_table_update(table_update)?, "pk_string" => db_update.pk_string = pk_string_table::parse_table_update(table_update)?, "pk_u128" => db_update.pk_u_128 = pk_u_128_table::parse_table_update(table_update)?, "pk_u16" => db_update.pk_u_16 = pk_u_16_table::parse_table_update(table_update)?, @@ -2975,6 +3041,8 @@ impl __sdk::DbUpdate for DbUpdate { let mut diff = AppliedDiff::default(); diff.btree_u_32 = cache.apply_diff_to_table::("btree_u32", &self.btree_u_32); + diff.indexed_simple_enum = + cache.apply_diff_to_table::("indexed_simple_enum", &self.indexed_simple_enum); diff.indexed_table = cache.apply_diff_to_table::("indexed_table", &self.indexed_table); diff.indexed_table_2 = cache.apply_diff_to_table::("indexed_table_2", &self.indexed_table_2); diff.large_table = cache.apply_diff_to_table::("large_table", &self.large_table); @@ -3047,6 +3115,9 @@ impl __sdk::DbUpdate for DbUpdate { diff.pk_identity = cache .apply_diff_to_table::("pk_identity", &self.pk_identity) .with_updates_by_pk(|row| &row.i); + diff.pk_simple_enum = cache + .apply_diff_to_table::("pk_simple_enum", &self.pk_simple_enum) + .with_updates_by_pk(|row| &row.a); diff.pk_string = cache .apply_diff_to_table::("pk_string", &self.pk_string) .with_updates_by_pk(|row| &row.s); @@ -3137,6 +3208,7 @@ impl __sdk::DbUpdate for DbUpdate { #[doc(hidden)] pub struct AppliedDiff<'r> { btree_u_32: __sdk::TableAppliedDiff<'r, BTreeU32>, + indexed_simple_enum: __sdk::TableAppliedDiff<'r, IndexedSimpleEnum>, indexed_table: __sdk::TableAppliedDiff<'r, IndexedTable>, indexed_table_2: __sdk::TableAppliedDiff<'r, IndexedTable2>, large_table: __sdk::TableAppliedDiff<'r, LargeTable>, @@ -3180,6 +3252,7 @@ pub struct AppliedDiff<'r> { pk_i_64: __sdk::TableAppliedDiff<'r, PkI64>, pk_i_8: __sdk::TableAppliedDiff<'r, PkI8>, pk_identity: __sdk::TableAppliedDiff<'r, PkIdentity>, + pk_simple_enum: __sdk::TableAppliedDiff<'r, PkSimpleEnum>, pk_string: __sdk::TableAppliedDiff<'r, PkString>, pk_u_128: __sdk::TableAppliedDiff<'r, PkU128>, pk_u_16: __sdk::TableAppliedDiff<'r, PkU16>, @@ -3241,6 +3314,11 @@ impl __sdk::InModule for AppliedDiff<'_> { impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { fn invoke_row_callbacks(&self, event: &EventContext, callbacks: &mut __sdk::DbCallbacks) { callbacks.invoke_table_row_callbacks::("btree_u32", &self.btree_u_32, event); + callbacks.invoke_table_row_callbacks::( + "indexed_simple_enum", + &self.indexed_simple_enum, + event, + ); callbacks.invoke_table_row_callbacks::("indexed_table", &self.indexed_table, event); callbacks.invoke_table_row_callbacks::("indexed_table_2", &self.indexed_table_2, event); callbacks.invoke_table_row_callbacks::("large_table", &self.large_table, event); @@ -3304,6 +3382,7 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { callbacks.invoke_table_row_callbacks::("pk_i64", &self.pk_i_64, event); callbacks.invoke_table_row_callbacks::("pk_i8", &self.pk_i_8, event); callbacks.invoke_table_row_callbacks::("pk_identity", &self.pk_identity, event); + callbacks.invoke_table_row_callbacks::("pk_simple_enum", &self.pk_simple_enum, event); callbacks.invoke_table_row_callbacks::("pk_string", &self.pk_string, event); callbacks.invoke_table_row_callbacks::("pk_u128", &self.pk_u_128, event); callbacks.invoke_table_row_callbacks::("pk_u16", &self.pk_u_16, event); @@ -3948,6 +4027,7 @@ impl __sdk::SpacetimeModule for RemoteModule { fn register_tables(client_cache: &mut __sdk::ClientCache) { btree_u_32_table::register_table(client_cache); + indexed_simple_enum_table::register_table(client_cache); indexed_table_table::register_table(client_cache); indexed_table_2_table::register_table(client_cache); large_table_table::register_table(client_cache); @@ -3991,6 +4071,7 @@ impl __sdk::SpacetimeModule for RemoteModule { pk_i_64_table::register_table(client_cache); pk_i_8_table::register_table(client_cache); pk_identity_table::register_table(client_cache); + pk_simple_enum_table::register_table(client_cache); pk_string_table::register_table(client_cache); pk_u_128_table::register_table(client_cache); pk_u_16_table::register_table(client_cache); diff --git a/crates/sdk/tests/test-client/src/module_bindings/pk_simple_enum_table.rs b/crates/sdk/tests/test-client/src/module_bindings/pk_simple_enum_table.rs new file mode 100644 index 00000000000..35abc5d6491 --- /dev/null +++ b/crates/sdk/tests/test-client/src/module_bindings/pk_simple_enum_table.rs @@ -0,0 +1,143 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::pk_simple_enum_type::PkSimpleEnum; +use super::simple_enum_type::SimpleEnum; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `pk_simple_enum`. +/// +/// Obtain a handle from the [`PkSimpleEnumTableAccess::pk_simple_enum`] method on [`super::RemoteTables`], +/// like `ctx.db.pk_simple_enum()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.pk_simple_enum().on_insert(...)`. +pub struct PkSimpleEnumTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `pk_simple_enum`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait PkSimpleEnumTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`PkSimpleEnumTableHandle`], which mediates access to the table `pk_simple_enum`. + fn pk_simple_enum(&self) -> PkSimpleEnumTableHandle<'_>; +} + +impl PkSimpleEnumTableAccess for super::RemoteTables { + fn pk_simple_enum(&self) -> PkSimpleEnumTableHandle<'_> { + PkSimpleEnumTableHandle { + imp: self.imp.get_table::("pk_simple_enum"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct PkSimpleEnumInsertCallbackId(__sdk::CallbackId); +pub struct PkSimpleEnumDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for PkSimpleEnumTableHandle<'ctx> { + type Row = PkSimpleEnum; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = PkSimpleEnumInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PkSimpleEnumInsertCallbackId { + PkSimpleEnumInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: PkSimpleEnumInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = PkSimpleEnumDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PkSimpleEnumDeleteCallbackId { + PkSimpleEnumDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: PkSimpleEnumDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("pk_simple_enum"); + _table.add_unique_constraint::("a", |row| &row.a); +} +pub struct PkSimpleEnumUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for PkSimpleEnumTableHandle<'ctx> { + type UpdateCallbackId = PkSimpleEnumUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> PkSimpleEnumUpdateCallbackId { + PkSimpleEnumUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: PkSimpleEnumUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::TableUpdate<__ws::BsatnFormat>, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +/// Access to the `a` unique index on the table `pk_simple_enum`, +/// which allows point queries on the field of the same name +/// via the [`PkSimpleEnumAUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.pk_simple_enum().a().find(...)`. +pub struct PkSimpleEnumAUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> PkSimpleEnumTableHandle<'ctx> { + /// Get a handle on the `a` unique index on the table `pk_simple_enum`. + pub fn a(&self) -> PkSimpleEnumAUnique<'ctx> { + PkSimpleEnumAUnique { + imp: self.imp.get_unique_constraint::("a"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> PkSimpleEnumAUnique<'ctx> { + /// Find the subscribed row whose `a` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &SimpleEnum) -> Option { + self.imp.find(col_val) + } +} diff --git a/crates/sdk/tests/test-client/src/module_bindings/pk_simple_enum_type.rs b/crates/sdk/tests/test-client/src/module_bindings/pk_simple_enum_type.rs new file mode 100644 index 00000000000..117828a1f00 --- /dev/null +++ b/crates/sdk/tests/test-client/src/module_bindings/pk_simple_enum_type.rs @@ -0,0 +1,18 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::simple_enum_type::SimpleEnum; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PkSimpleEnum { + pub a: SimpleEnum, + pub data: i32, +} + +impl __sdk::InModule for PkSimpleEnum { + type Module = super::RemoteModule; +} diff --git a/crates/sdk/tests/test-client/src/module_bindings/simple_enum_type.rs b/crates/sdk/tests/test-client/src/module_bindings/simple_enum_type.rs index a15c8994020..6899970316c 100644 --- a/crates/sdk/tests/test-client/src/module_bindings/simple_enum_type.rs +++ b/crates/sdk/tests/test-client/src/module_bindings/simple_enum_type.rs @@ -6,6 +6,7 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] +#[derive(Copy, Eq, Hash)] pub enum SimpleEnum { Zero, diff --git a/crates/sdk/tests/test-client/src/module_bindings/update_indexed_simple_enum_reducer.rs b/crates/sdk/tests/test-client/src/module_bindings/update_indexed_simple_enum_reducer.rs new file mode 100644 index 00000000000..d93d856aba4 --- /dev/null +++ b/crates/sdk/tests/test-client/src/module_bindings/update_indexed_simple_enum_reducer.rs @@ -0,0 +1,105 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::simple_enum_type::SimpleEnum; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub(super) struct UpdateIndexedSimpleEnumArgs { + pub a: SimpleEnum, + pub b: SimpleEnum, +} + +impl From for super::Reducer { + fn from(args: UpdateIndexedSimpleEnumArgs) -> Self { + Self::UpdateIndexedSimpleEnum { a: args.a, b: args.b } + } +} + +impl __sdk::InModule for UpdateIndexedSimpleEnumArgs { + type Module = super::RemoteModule; +} + +pub struct UpdateIndexedSimpleEnumCallbackId(__sdk::CallbackId); + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `update_indexed_simple_enum`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait update_indexed_simple_enum { + /// Request that the remote module invoke the reducer `update_indexed_simple_enum` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed by listening for [`Self::on_update_indexed_simple_enum`] callbacks. + fn update_indexed_simple_enum(&self, a: SimpleEnum, b: SimpleEnum) -> __sdk::Result<()>; + /// Register a callback to run whenever we are notified of an invocation of the reducer `update_indexed_simple_enum`. + /// + /// Callbacks should inspect the [`__sdk::ReducerEvent`] contained in the [`super::ReducerEventContext`] + /// to determine the reducer's status. + /// + /// The returned [`UpdateIndexedSimpleEnumCallbackId`] can be passed to [`Self::remove_on_update_indexed_simple_enum`] + /// to cancel the callback. + fn on_update_indexed_simple_enum( + &self, + callback: impl FnMut(&super::ReducerEventContext, &SimpleEnum, &SimpleEnum) + Send + 'static, + ) -> UpdateIndexedSimpleEnumCallbackId; + /// Cancel a callback previously registered by [`Self::on_update_indexed_simple_enum`], + /// causing it not to run in the future. + fn remove_on_update_indexed_simple_enum(&self, callback: UpdateIndexedSimpleEnumCallbackId); +} + +impl update_indexed_simple_enum for super::RemoteReducers { + fn update_indexed_simple_enum(&self, a: SimpleEnum, b: SimpleEnum) -> __sdk::Result<()> { + self.imp + .call_reducer("update_indexed_simple_enum", UpdateIndexedSimpleEnumArgs { a, b }) + } + fn on_update_indexed_simple_enum( + &self, + mut callback: impl FnMut(&super::ReducerEventContext, &SimpleEnum, &SimpleEnum) + Send + 'static, + ) -> UpdateIndexedSimpleEnumCallbackId { + UpdateIndexedSimpleEnumCallbackId(self.imp.on_reducer( + "update_indexed_simple_enum", + Box::new(move |ctx: &super::ReducerEventContext| { + let super::ReducerEventContext { + event: + __sdk::ReducerEvent { + reducer: super::Reducer::UpdateIndexedSimpleEnum { a, b }, + .. + }, + .. + } = ctx + else { + unreachable!() + }; + callback(ctx, a, b) + }), + )) + } + fn remove_on_update_indexed_simple_enum(&self, callback: UpdateIndexedSimpleEnumCallbackId) { + self.imp.remove_on_reducer("update_indexed_simple_enum", callback.0) + } +} + +#[allow(non_camel_case_types)] +#[doc(hidden)] +/// Extension trait for setting the call-flags for the reducer `update_indexed_simple_enum`. +/// +/// Implemented for [`super::SetReducerFlags`]. +/// +/// This type is currently unstable and may be removed without a major version bump. +pub trait set_flags_for_update_indexed_simple_enum { + /// Set the call-reducer flags for the reducer `update_indexed_simple_enum` to `flags`. + /// + /// This type is currently unstable and may be removed without a major version bump. + fn update_indexed_simple_enum(&self, flags: __ws::CallReducerFlags); +} + +impl set_flags_for_update_indexed_simple_enum for super::SetReducerFlags { + fn update_indexed_simple_enum(&self, flags: __ws::CallReducerFlags) { + self.imp.set_call_reducer_flags("update_indexed_simple_enum", flags); + } +} diff --git a/crates/sdk/tests/test-client/src/module_bindings/update_pk_simple_enum_reducer.rs b/crates/sdk/tests/test-client/src/module_bindings/update_pk_simple_enum_reducer.rs new file mode 100644 index 00000000000..80222f53fa0 --- /dev/null +++ b/crates/sdk/tests/test-client/src/module_bindings/update_pk_simple_enum_reducer.rs @@ -0,0 +1,108 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::simple_enum_type::SimpleEnum; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub(super) struct UpdatePkSimpleEnumArgs { + pub a: SimpleEnum, + pub data: i32, +} + +impl From for super::Reducer { + fn from(args: UpdatePkSimpleEnumArgs) -> Self { + Self::UpdatePkSimpleEnum { + a: args.a, + data: args.data, + } + } +} + +impl __sdk::InModule for UpdatePkSimpleEnumArgs { + type Module = super::RemoteModule; +} + +pub struct UpdatePkSimpleEnumCallbackId(__sdk::CallbackId); + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `update_pk_simple_enum`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait update_pk_simple_enum { + /// Request that the remote module invoke the reducer `update_pk_simple_enum` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed by listening for [`Self::on_update_pk_simple_enum`] callbacks. + fn update_pk_simple_enum(&self, a: SimpleEnum, data: i32) -> __sdk::Result<()>; + /// Register a callback to run whenever we are notified of an invocation of the reducer `update_pk_simple_enum`. + /// + /// Callbacks should inspect the [`__sdk::ReducerEvent`] contained in the [`super::ReducerEventContext`] + /// to determine the reducer's status. + /// + /// The returned [`UpdatePkSimpleEnumCallbackId`] can be passed to [`Self::remove_on_update_pk_simple_enum`] + /// to cancel the callback. + fn on_update_pk_simple_enum( + &self, + callback: impl FnMut(&super::ReducerEventContext, &SimpleEnum, &i32) + Send + 'static, + ) -> UpdatePkSimpleEnumCallbackId; + /// Cancel a callback previously registered by [`Self::on_update_pk_simple_enum`], + /// causing it not to run in the future. + fn remove_on_update_pk_simple_enum(&self, callback: UpdatePkSimpleEnumCallbackId); +} + +impl update_pk_simple_enum for super::RemoteReducers { + fn update_pk_simple_enum(&self, a: SimpleEnum, data: i32) -> __sdk::Result<()> { + self.imp + .call_reducer("update_pk_simple_enum", UpdatePkSimpleEnumArgs { a, data }) + } + fn on_update_pk_simple_enum( + &self, + mut callback: impl FnMut(&super::ReducerEventContext, &SimpleEnum, &i32) + Send + 'static, + ) -> UpdatePkSimpleEnumCallbackId { + UpdatePkSimpleEnumCallbackId(self.imp.on_reducer( + "update_pk_simple_enum", + Box::new(move |ctx: &super::ReducerEventContext| { + let super::ReducerEventContext { + event: + __sdk::ReducerEvent { + reducer: super::Reducer::UpdatePkSimpleEnum { a, data }, + .. + }, + .. + } = ctx + else { + unreachable!() + }; + callback(ctx, a, data) + }), + )) + } + fn remove_on_update_pk_simple_enum(&self, callback: UpdatePkSimpleEnumCallbackId) { + self.imp.remove_on_reducer("update_pk_simple_enum", callback.0) + } +} + +#[allow(non_camel_case_types)] +#[doc(hidden)] +/// Extension trait for setting the call-flags for the reducer `update_pk_simple_enum`. +/// +/// Implemented for [`super::SetReducerFlags`]. +/// +/// This type is currently unstable and may be removed without a major version bump. +pub trait set_flags_for_update_pk_simple_enum { + /// Set the call-reducer flags for the reducer `update_pk_simple_enum` to `flags`. + /// + /// This type is currently unstable and may be removed without a major version bump. + fn update_pk_simple_enum(&self, flags: __ws::CallReducerFlags); +} + +impl set_flags_for_update_pk_simple_enum for super::SetReducerFlags { + fn update_pk_simple_enum(&self, flags: __ws::CallReducerFlags) { + self.imp.set_call_reducer_flags("update_pk_simple_enum", flags); + } +} diff --git a/crates/sdk/tests/test.rs b/crates/sdk/tests/test.rs index 99750fd9eea..584371bf626 100644 --- a/crates/sdk/tests/test.rs +++ b/crates/sdk/tests/test.rs @@ -235,6 +235,16 @@ macro_rules! declare_tests_with_suffix { fn test_rls_subscription() { make_test("test-rls-subscription").run() } + + #[test] + fn pk_simple_enum() { + make_test("pk-simple-enum").run(); + } + + #[test] + fn indexed_simple_enum() { + make_test("indexed-simple-enum").run(); + } } }; } diff --git a/crates/table/proptest-regressions/table_index/unique_directer_index.txt b/crates/table/proptest-regressions/table_index/unique_directer_index.txt new file mode 100644 index 00000000000..1d50a748347 --- /dev/null +++ b/crates/table/proptest-regressions/table_index/unique_directer_index.txt @@ -0,0 +1,10 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 471733dcdf80f2858344c15b6297070a312d2051ecf0eb56eaf9bf38cd51aab4 # shrinks to start = 38, end = 0 +cc 98241ddfa4ab59c91a264cbbd364619c1d02a912406c4402cd01fb0c10a72e10 # shrinks to start = 75, end = 0 +cc b058bb0beeaf3bb4f97854dff2bf835e2ea4eaffb1eb3c13c78652018b02b957 # shrinks to start = 130, end = 0, key = 0 +cc dc8757b81c7b7cffe9e4a9d43b75cd5814d5fb0667a0c1f401fd5f402fde3d97 # shrinks to start = 37, end = 37, key = 0 diff --git a/crates/table/src/memory_usage.rs b/crates/table/src/memory_usage.rs index 6822f2f3a41..2cecce35dd6 100644 --- a/crates/table/src/memory_usage.rs +++ b/crates/table/src/memory_usage.rs @@ -39,6 +39,12 @@ impl MemoryUsage for f64 {} impl MemoryUsage for spacetimedb_sats::F32 {} impl MemoryUsage for spacetimedb_sats::F64 {} +impl MemoryUsage for &T { + fn heap_usage(&self) -> usize { + (*self).heap_usage() + } +} + impl MemoryUsage for Box { fn heap_usage(&self) -> usize { mem::size_of_val::(self) + T::heap_usage(self) @@ -65,6 +71,12 @@ impl MemoryUsage for [T] { } } +impl MemoryUsage for [T; N] { + fn heap_usage(&self) -> usize { + self.iter().map(T::heap_usage).sum() + } +} + impl MemoryUsage for str {} impl MemoryUsage for Option { diff --git a/crates/table/src/read_column.rs b/crates/table/src/read_column.rs index 1295d01e621..000ada6ec4a 100644 --- a/crates/table/src/read_column.rs +++ b/crates/table/src/read_column.rs @@ -11,7 +11,9 @@ use crate::{ }; use spacetimedb_sats::{ algebraic_value::{ser::ValueSerializer, Packed}, - i256, u256, AlgebraicType, AlgebraicValue, ArrayValue, ProductType, ProductValue, SumValue, + i256, + sum_value::SumTag, + u256, AlgebraicType, AlgebraicValue, ArrayValue, ProductType, ProductValue, SumValue, }; use std::{cell::Cell, mem}; use thiserror::Error; @@ -339,6 +341,29 @@ impl_read_column_via_from! { i256 => Box; } +/// SAFETY: `is_compatible_type` only returns true for sum types, +/// and any sum value stores the tag first in BFLATN. +unsafe impl ReadColumn for SumTag { + fn is_compatible_type(ty: &AlgebraicTypeLayout) -> bool { + matches!(ty, AlgebraicTypeLayout::Sum(_)) + } + + unsafe fn unchecked_read_column(row_ref: RowRef<'_>, layout: &ProductTypeElementLayout) -> Self { + debug_assert!(Self::is_compatible_type(&layout.ty)); + + let (page, offset) = row_ref.page_and_offset(); + let col_offset = offset + PageOffset(layout.offset); + + let data = page.get_row_data(col_offset, Size(1)); + let data: Result<[u8; 1], _> = data.try_into(); + // SAFETY: `<[u8; 1] as TryFrom<&[u8]>` succeeds if and only if the slice's length is `1`. + // We used `1` as both the length of the slice and the array, so we know them to be equal. + let [data] = unsafe { data.unwrap_unchecked() }; + + Self(data) + } +} + #[cfg(test)] mod test { use super::*; @@ -512,5 +537,21 @@ mod test { // Use a long string which will hit the blob store. read_column_long_string { AlgebraicType::String => Box = "long string. ".repeat(2048).into() }; + + read_sum_value_plain { AlgebraicType::simple_enum(["a", "b"].into_iter()) => SumValue = SumValue::new_simple(1) }; + read_sum_tag_plain { AlgebraicType::simple_enum(["a", "b"].into_iter()) => SumTag = SumTag(1) }; + } + + #[test] + fn read_sum_tag_from_sum_with_payload() { + let algebraic_type = AlgebraicType::sum([("a", AlgebraicType::U8), ("b", AlgebraicType::U16)]); + + let mut blob_store = HashMapBlobStore::default(); + let mut table = table(ProductType::from([algebraic_type])); + + let val = SumValue::new(1, 42u16); + let (_, row_ref) = table.insert(&mut blob_store, &product![val.clone()]).unwrap(); + + assert_eq!(val.tag, row_ref.read_col::(0).unwrap().0); } } diff --git a/crates/table/src/table_index/mod.rs b/crates/table/src/table_index/mod.rs index 29edc4f0f15..24ab869bc69 100644 --- a/crates/table/src/table_index/mod.rs +++ b/crates/table/src/table_index/mod.rs @@ -30,16 +30,19 @@ use crate::{read_column::ReadColumn, static_assert_size, MemoryUsage}; use core::ops::RangeBounds; use spacetimedb_primitives::ColList; use spacetimedb_sats::{ - algebraic_value::Packed, i256, product_value::InvalidFieldError, u256, AlgebraicType, AlgebraicValue, ProductType, + algebraic_value::Packed, i256, product_value::InvalidFieldError, sum_value::SumTag, u256, AlgebraicType, + AlgebraicValue, ProductType, }; mod key_size; mod multimap; +pub mod unique_direct_fixed_cap_index; pub mod unique_direct_index; pub mod uniquemap; pub use key_size::KeySize; use spacetimedb_schema::def::IndexAlgorithm; +use unique_direct_fixed_cap_index::{UniqueDirectFixedCapIndex, UniqueDirectFixedCapIndexRangeIter}; use unique_direct_index::{UniqueDirectIndex, UniqueDirectIndexPointIter, UniqueDirectIndexRangeIter}; type BtreeIndex = multimap::MultiMap; @@ -122,6 +125,7 @@ enum TypedIndexRangeIter<'a> { UniqueBtreeAV(BtreeUniqueIndexRangeIter<'a, AlgebraicValue>), UniqueDirect(UniqueDirectIndexRangeIter<'a>), + UniqueDirectU8(UniqueDirectFixedCapIndexRangeIter<'a>), } impl Iterator for TypedIndexRangeIter<'_> { @@ -161,6 +165,7 @@ impl Iterator for TypedIndexRangeIter<'_> { Self::UniqueBtreeAV(this) => this.next().copied(), Self::UniqueDirect(this) => this.next(), + Self::UniqueDirectU8(this) => this.next(), } } } @@ -187,6 +192,7 @@ enum TypedIndex { // All the non-unique btree index types. BtreeBool(BtreeIndex), BtreeU8(BtreeIndex), + BtreeSumTag(BtreeIndex), BtreeI8(BtreeIndex), BtreeU16(BtreeIndex), BtreeI16(BtreeIndex), @@ -204,6 +210,7 @@ enum TypedIndex { // All the unique btree index types. UniqueBtreeBool(BtreeUniqueIndex), UniqueBtreeU8(BtreeUniqueIndex), + UniqueBtreeSumTag(BtreeUniqueIndex), UniqueBtreeI8(BtreeUniqueIndex), UniqueBtreeU16(BtreeUniqueIndex), UniqueBtreeI16(BtreeUniqueIndex), @@ -220,6 +227,7 @@ enum TypedIndex { // All the unique direct index types. UniqueDirectU8(UniqueDirectIndex), + UniqueDirectSumTag(UniqueDirectFixedCapIndex), UniqueDirectU16(UniqueDirectIndex), UniqueDirectU32(UniqueDirectIndex), UniqueDirectU64(UniqueDirectIndex), @@ -229,7 +237,7 @@ impl MemoryUsage for TypedIndex { fn heap_usage(&self) -> usize { match self { TypedIndex::BtreeBool(this) => this.heap_usage(), - TypedIndex::BtreeU8(this) => this.heap_usage(), + TypedIndex::BtreeU8(this) | TypedIndex::BtreeSumTag(this) => this.heap_usage(), TypedIndex::BtreeI8(this) => this.heap_usage(), TypedIndex::BtreeU16(this) => this.heap_usage(), TypedIndex::BtreeI16(this) => this.heap_usage(), @@ -245,7 +253,7 @@ impl MemoryUsage for TypedIndex { TypedIndex::BtreeAV(this) => this.heap_usage(), TypedIndex::UniqueBtreeBool(this) => this.heap_usage(), - TypedIndex::UniqueBtreeU8(this) => this.heap_usage(), + TypedIndex::UniqueBtreeU8(this) | TypedIndex::UniqueBtreeSumTag(this) => this.heap_usage(), TypedIndex::UniqueBtreeI8(this) => this.heap_usage(), TypedIndex::UniqueBtreeU16(this) => this.heap_usage(), TypedIndex::UniqueBtreeI16(this) => this.heap_usage(), @@ -260,6 +268,7 @@ impl MemoryUsage for TypedIndex { TypedIndex::UniqueBtreeString(this) => this.heap_usage(), TypedIndex::UniqueBtreeAV(this) => this.heap_usage(), + TypedIndex::UniqueDirectSumTag(this) => this.heap_usage(), TypedIndex::UniqueDirectU8(this) | TypedIndex::UniqueDirectU16(this) | TypedIndex::UniqueDirectU32(this) @@ -268,6 +277,10 @@ impl MemoryUsage for TypedIndex { } } +fn as_tag(av: &AlgebraicValue) -> Option<&u8> { + av.as_sum().map(|s| &s.tag) +} + impl TypedIndex { /// Returns a new index with keys being of `key_type` and the index possibly `is_unique`. fn new(key_type: &AlgebraicType, index_algo: &IndexAlgorithm, is_unique: bool) -> Self { @@ -280,11 +293,15 @@ impl TypedIndex { AlgebraicType::U16 => Self::UniqueDirectU16(<_>::default()), AlgebraicType::U32 => Self::UniqueDirectU32(<_>::default()), AlgebraicType::U64 => Self::UniqueDirectU64(<_>::default()), + // For a plain enum, use `u8` as the native type. + AlgebraicType::Sum(sum) if sum.is_simple_enum() => { + UniqueDirectSumTag(UniqueDirectFixedCapIndex::new(sum.variants.len())) + } _ => unreachable!("unexpected key type {key_type:?} for direct index"), }; } - // If the index is on a single column of a primitive type, + // If the index is on a single column of a primitive type, string, or plain enum, // use a homogeneous map with a native key type. if is_unique { match key_type { @@ -302,6 +319,9 @@ impl TypedIndex { AlgebraicType::I256 => UniqueBtreeI256(<_>::default()), AlgebraicType::U256 => UniqueBtreeU256(<_>::default()), AlgebraicType::String => UniqueBtreeString(<_>::default()), + // For a plain enum, use `u8` as the native type. + // We use a direct index here + AlgebraicType::Sum(sum) if sum.is_simple_enum() => UniqueBtreeSumTag(<_>::default()), // The index is either multi-column, // or we don't care to specialize on the key type, @@ -325,6 +345,9 @@ impl TypedIndex { AlgebraicType::U256 => BtreeU256(<_>::default()), AlgebraicType::String => BtreeString(<_>::default()), + // For a plain enum, use `u8` as the native type. + AlgebraicType::Sum(sum) if sum.is_simple_enum() => BtreeSumTag(<_>::default()), + // The index is either multi-column, // or we don't care to specialize on the key type, // so use a map keyed on `AlgebraicValue`. @@ -340,6 +363,7 @@ impl TypedIndex { match self { BtreeBool(_) => BtreeBool(<_>::default()), BtreeU8(_) => BtreeU8(<_>::default()), + BtreeSumTag(_) => BtreeSumTag(<_>::default()), BtreeI8(_) => BtreeI8(<_>::default()), BtreeU16(_) => BtreeU16(<_>::default()), BtreeI16(_) => BtreeI16(<_>::default()), @@ -355,6 +379,7 @@ impl TypedIndex { BtreeAV(_) => BtreeAV(<_>::default()), UniqueBtreeBool(_) => UniqueBtreeBool(<_>::default()), UniqueBtreeU8(_) => UniqueBtreeU8(<_>::default()), + UniqueBtreeSumTag(_) => UniqueBtreeSumTag(<_>::default()), UniqueBtreeI8(_) => UniqueBtreeI8(<_>::default()), UniqueBtreeU16(_) => UniqueBtreeU16(<_>::default()), UniqueBtreeI16(_) => UniqueBtreeI16(<_>::default()), @@ -369,6 +394,7 @@ impl TypedIndex { UniqueBtreeString(_) => UniqueBtreeString(<_>::default()), UniqueBtreeAV(_) => UniqueBtreeAV(<_>::default()), UniqueDirectU8(_) => UniqueDirectU8(<_>::default()), + UniqueDirectSumTag(idx) => UniqueDirectSumTag(idx.clone_structure()), UniqueDirectU16(_) => UniqueDirectU16(<_>::default()), UniqueDirectU32(_) => UniqueDirectU32(<_>::default()), UniqueDirectU64(_) => UniqueDirectU64(<_>::default()), @@ -379,15 +405,30 @@ impl TypedIndex { fn is_unique(&self) -> bool { use TypedIndex::*; match self { - BtreeBool(_) | BtreeU8(_) | BtreeI8(_) | BtreeU16(_) | BtreeI16(_) | BtreeU32(_) | BtreeI32(_) - | BtreeU64(_) | BtreeI64(_) | BtreeU128(_) | BtreeI128(_) | BtreeU256(_) | BtreeI256(_) + BtreeBool(_) | BtreeU8(_) | BtreeSumTag(_) | BtreeI8(_) | BtreeU16(_) | BtreeI16(_) | BtreeU32(_) + | BtreeI32(_) | BtreeU64(_) | BtreeI64(_) | BtreeU128(_) | BtreeI128(_) | BtreeU256(_) | BtreeI256(_) | BtreeString(_) | BtreeAV(_) => false, - UniqueBtreeBool(_) | UniqueBtreeU8(_) | UniqueBtreeI8(_) | UniqueBtreeU16(_) | UniqueBtreeI16(_) - | UniqueBtreeU32(_) | UniqueBtreeI32(_) | UniqueBtreeU64(_) | UniqueBtreeI64(_) | UniqueBtreeU128(_) - | UniqueBtreeI128(_) | UniqueBtreeU256(_) | UniqueBtreeI256(_) | UniqueBtreeString(_) - | UniqueBtreeAV(_) | UniqueDirectU8(_) | UniqueDirectU16(_) | UniqueDirectU32(_) | UniqueDirectU64(_) => { - true - } + UniqueBtreeBool(_) + | UniqueBtreeU8(_) + | UniqueBtreeSumTag(_) + | UniqueBtreeI8(_) + | UniqueBtreeU16(_) + | UniqueBtreeI16(_) + | UniqueBtreeU32(_) + | UniqueBtreeI32(_) + | UniqueBtreeU64(_) + | UniqueBtreeI64(_) + | UniqueBtreeU128(_) + | UniqueBtreeI128(_) + | UniqueBtreeU256(_) + | UniqueBtreeI256(_) + | UniqueBtreeString(_) + | UniqueBtreeAV(_) + | UniqueDirectU8(_) + | UniqueDirectSumTag(_) + | UniqueDirectU16(_) + | UniqueDirectU32(_) + | UniqueDirectU64(_) => true, } } @@ -469,9 +510,26 @@ impl TypedIndex { let key_size = key.key_size_in_bytes(); this.insert(key, row_ref.pointer()).map(|_| key_size) } + fn direct_u8_insert_at_type( + this: &mut UniqueDirectFixedCapIndex, + cols: &ColList, + row_ref: RowRef<'_>, + to_u8: impl FnOnce(T) -> usize, + ) -> Result { + let key: T = project_to_singleton_key(cols, row_ref); + let key = to_u8(key); + let key_size = key.key_size_in_bytes(); + this.insert(key, row_ref.pointer()).map(|_| key_size) + } match self { Self::BtreeBool(idx) => mm_insert_at_type(idx, cols, row_ref), Self::BtreeU8(idx) => mm_insert_at_type(idx, cols, row_ref), + Self::BtreeSumTag(idx) => { + let SumTag(key) = project_to_singleton_key(cols, row_ref); + let key_size = key.key_size_in_bytes(); + idx.insert(key, row_ref.pointer()); + Ok(key_size) + } Self::BtreeI8(idx) => mm_insert_at_type(idx, cols, row_ref), Self::BtreeU16(idx) => mm_insert_at_type(idx, cols, row_ref), Self::BtreeI16(idx) => mm_insert_at_type(idx, cols, row_ref), @@ -493,6 +551,11 @@ impl TypedIndex { } Self::UniqueBtreeBool(idx) => um_insert_at_type(idx, cols, row_ref), Self::UniqueBtreeU8(idx) => um_insert_at_type(idx, cols, row_ref), + Self::UniqueBtreeSumTag(idx) => { + let SumTag(key) = project_to_singleton_key(cols, row_ref); + let key_size = key.key_size_in_bytes(); + idx.insert(key, row_ref.pointer()).map_err(|ptr| *ptr).map(|_| key_size) + } Self::UniqueBtreeI8(idx) => um_insert_at_type(idx, cols, row_ref), Self::UniqueBtreeU16(idx) => um_insert_at_type(idx, cols, row_ref), Self::UniqueBtreeI16(idx) => um_insert_at_type(idx, cols, row_ref), @@ -513,6 +576,7 @@ impl TypedIndex { .map_err(|ptr| *ptr) .map(|_| key_size) } + Self::UniqueDirectSumTag(idx) => direct_u8_insert_at_type(idx, cols, row_ref, |SumTag(tag)| tag as usize), Self::UniqueDirectU8(idx) => direct_insert_at_type(idx, cols, row_ref, |k: u8| k as usize), Self::UniqueDirectU16(idx) => direct_insert_at_type(idx, cols, row_ref, |k: u16| k as usize), Self::UniqueDirectU32(idx) => direct_insert_at_type(idx, cols, row_ref, |k: u32| k as usize), @@ -536,6 +600,7 @@ impl TypedIndex { /// We want to store said counter outside of the [`TypedIndex`] enum, /// but we can only compute the size using type info within the [`TypedIndex`], /// so we have to return the size across this boundary. + // TODO(centril): make this unsafe and use unchecked conversions. fn delete(&mut self, cols: &ColList, row_ref: RowRef<'_>) -> Result, InvalidFieldError> { fn mm_delete_at_type( this: &mut BtreeIndex, @@ -569,10 +634,28 @@ impl TypedIndex { let key_size = key.key_size_in_bytes(); Ok(this.delete(key).then_some(key_size)) } + fn direct_u8_delete_at_type( + this: &mut UniqueDirectFixedCapIndex, + cols: &ColList, + row_ref: RowRef<'_>, + to_u8: impl FnOnce(T) -> usize, + ) -> Result, InvalidFieldError> { + let col_pos = cols.as_singleton().unwrap(); + let key: T = row_ref.read_col(col_pos).map_err(|_| col_pos)?; + let key = to_u8(key); + let key_size = key.key_size_in_bytes(); + Ok(this.delete(key).then_some(key_size)) + } match self { Self::BtreeBool(this) => mm_delete_at_type(this, cols, row_ref), Self::BtreeU8(this) => mm_delete_at_type(this, cols, row_ref), + Self::BtreeSumTag(this) => { + let col_pos = cols.as_singleton().unwrap(); + let SumTag(key) = row_ref.read_col(col_pos).map_err(|_| col_pos)?; + let key_size = key.key_size_in_bytes(); + Ok(this.delete(&key, &row_ref.pointer()).then_some(key_size)) + } Self::BtreeI8(this) => mm_delete_at_type(this, cols, row_ref), Self::BtreeU16(this) => mm_delete_at_type(this, cols, row_ref), Self::BtreeI16(this) => mm_delete_at_type(this, cols, row_ref), @@ -592,6 +675,12 @@ impl TypedIndex { } Self::UniqueBtreeBool(this) => um_delete_at_type(this, cols, row_ref), Self::UniqueBtreeU8(this) => um_delete_at_type(this, cols, row_ref), + Self::UniqueBtreeSumTag(this) => { + let col_pos = cols.as_singleton().unwrap(); + let SumTag(key) = row_ref.read_col(col_pos).map_err(|_| col_pos)?; + let key_size = key.key_size_in_bytes(); + Ok(this.delete(&key).then_some(key_size)) + } Self::UniqueBtreeI8(this) => um_delete_at_type(this, cols, row_ref), Self::UniqueBtreeU16(this) => um_delete_at_type(this, cols, row_ref), Self::UniqueBtreeI16(this) => um_delete_at_type(this, cols, row_ref), @@ -609,6 +698,7 @@ impl TypedIndex { let key_size = key.key_size_in_bytes(); Ok(this.delete(&key).then_some(key_size)) } + Self::UniqueDirectSumTag(this) => direct_u8_delete_at_type(this, cols, row_ref, |SumTag(k)| k as usize), Self::UniqueDirectU8(this) => direct_delete_at_type(this, cols, row_ref, |k: u8| k as usize), Self::UniqueDirectU16(this) => direct_delete_at_type(this, cols, row_ref, |k: u16| k as usize), Self::UniqueDirectU32(this) => direct_delete_at_type(this, cols, row_ref, |k: u32| k as usize), @@ -646,6 +736,7 @@ impl TypedIndex { match self { BtreeBool(this) => BTree(mm_iter_at_type(this, key, AlgebraicValue::as_bool)), BtreeU8(this) => BTree(mm_iter_at_type(this, key, AlgebraicValue::as_u8)), + BtreeSumTag(this) => BTree(mm_iter_at_type(this, key, as_tag)), BtreeI8(this) => BTree(mm_iter_at_type(this, key, AlgebraicValue::as_i8)), BtreeU16(this) => BTree(mm_iter_at_type(this, key, AlgebraicValue::as_u16)), BtreeI16(this) => BTree(mm_iter_at_type(this, key, AlgebraicValue::as_i16)), @@ -662,6 +753,7 @@ impl TypedIndex { UniqueBtreeBool(this) => UniqueBTree(um_iter_at_type(this, key, AlgebraicValue::as_bool)), UniqueBtreeU8(this) => UniqueBTree(um_iter_at_type(this, key, AlgebraicValue::as_u8)), + UniqueBtreeSumTag(this) => UniqueBTree(um_iter_at_type(this, key, as_tag)), UniqueBtreeI8(this) => UniqueBTree(um_iter_at_type(this, key, AlgebraicValue::as_i8)), UniqueBtreeU16(this) => UniqueBTree(um_iter_at_type(this, key, AlgebraicValue::as_u16)), UniqueBtreeI16(this) => UniqueBTree(um_iter_at_type(this, key, AlgebraicValue::as_i16)), @@ -676,6 +768,10 @@ impl TypedIndex { UniqueBtreeString(this) => UniqueBTree(um_iter_at_type(this, key, AlgebraicValue::as_string)), UniqueBtreeAV(this) => UniqueBTree(this.values_in_point(key)), + UniqueDirectSumTag(this) => { + let key = as_tag(key).expect("key does not conform to key type of index"); + UniqueDirect(this.seek_point(*key as usize)) + } UniqueDirectU8(this) => { UniqueDirect(direct_iter_at_type(this, key, AlgebraicValue::as_u8, |k| *k as usize)) } @@ -728,6 +824,7 @@ impl TypedIndex { match self { Self::BtreeBool(this) => BtreeBool(mm_iter_at_type(this, range, AlgebraicValue::as_bool)), Self::BtreeU8(this) => BtreeU8(mm_iter_at_type(this, range, AlgebraicValue::as_u8)), + Self::BtreeSumTag(this) => BtreeU8(mm_iter_at_type(this, range, as_tag)), Self::BtreeI8(this) => BtreeI8(mm_iter_at_type(this, range, AlgebraicValue::as_i8)), Self::BtreeU16(this) => BtreeU16(mm_iter_at_type(this, range, AlgebraicValue::as_u16)), Self::BtreeI16(this) => BtreeI16(mm_iter_at_type(this, range, AlgebraicValue::as_i16)), @@ -744,6 +841,7 @@ impl TypedIndex { Self::UniqueBtreeBool(this) => UniqueBtreeBool(um_iter_at_type(this, range, AlgebraicValue::as_bool)), Self::UniqueBtreeU8(this) => UniqueBtreeU8(um_iter_at_type(this, range, AlgebraicValue::as_u8)), + Self::UniqueBtreeSumTag(this) => UniqueBtreeU8(um_iter_at_type(this, range, as_tag)), Self::UniqueBtreeI8(this) => UniqueBtreeI8(um_iter_at_type(this, range, AlgebraicValue::as_i8)), Self::UniqueBtreeU16(this) => UniqueBtreeU16(um_iter_at_type(this, range, AlgebraicValue::as_u16)), Self::UniqueBtreeI16(this) => UniqueBtreeI16(um_iter_at_type(this, range, AlgebraicValue::as_i16)), @@ -762,6 +860,13 @@ impl TypedIndex { Self::UniqueBtreeString(this) => UniqueBtreeString(um_iter_at_type(this, range, AlgebraicValue::as_string)), Self::UniqueBtreeAV(this) => UniqueBtreeAV(this.values_in_range(range)), + Self::UniqueDirectSumTag(this) => { + let av_as_t = |v| as_tag(v).copied().expect("bound does not conform to key type of index") as usize; + let start = range.start_bound().map(av_as_t); + let end = range.end_bound().map(av_as_t); + let iter = this.seek_range(&(start, end)); + UniqueDirectU8(iter) + } Self::UniqueDirectU8(this) => { UniqueDirect(direct_iter_at_type(this, range, AlgebraicValue::as_u8, |k| *k as usize)) } @@ -786,7 +891,7 @@ impl TypedIndex { fn clear(&mut self) { match self { Self::BtreeBool(this) => this.clear(), - Self::BtreeU8(this) => this.clear(), + Self::BtreeU8(this) | Self::BtreeSumTag(this) => this.clear(), Self::BtreeI8(this) => this.clear(), Self::BtreeU16(this) => this.clear(), Self::BtreeI16(this) => this.clear(), @@ -802,7 +907,7 @@ impl TypedIndex { Self::BtreeAV(this) => this.clear(), Self::UniqueBtreeBool(this) => this.clear(), - Self::UniqueBtreeU8(this) => this.clear(), + Self::UniqueBtreeU8(this) | Self::UniqueBtreeSumTag(this) => this.clear(), Self::UniqueBtreeI8(this) => this.clear(), Self::UniqueBtreeU16(this) => this.clear(), Self::UniqueBtreeI16(this) => this.clear(), @@ -817,6 +922,7 @@ impl TypedIndex { Self::UniqueBtreeString(this) => this.clear(), Self::UniqueBtreeAV(this) => this.clear(), + Self::UniqueDirectSumTag(this) => this.clear(), Self::UniqueDirectU8(this) | Self::UniqueDirectU16(this) | Self::UniqueDirectU32(this) @@ -833,7 +939,7 @@ impl TypedIndex { fn len(&self) -> usize { match self { Self::BtreeBool(this) => this.len(), - Self::BtreeU8(this) => this.len(), + Self::BtreeU8(this) | Self::BtreeSumTag(this) => this.len(), Self::BtreeI8(this) => this.len(), Self::BtreeU16(this) => this.len(), Self::BtreeI16(this) => this.len(), @@ -849,7 +955,7 @@ impl TypedIndex { Self::BtreeAV(this) => this.len(), Self::UniqueBtreeBool(this) => this.len(), - Self::UniqueBtreeU8(this) => this.len(), + Self::UniqueBtreeU8(this) | Self::UniqueBtreeSumTag(this) => this.len(), Self::UniqueBtreeI8(this) => this.len(), Self::UniqueBtreeU16(this) => this.len(), Self::UniqueBtreeI16(this) => this.len(), @@ -864,6 +970,7 @@ impl TypedIndex { Self::UniqueBtreeString(this) => this.len(), Self::UniqueBtreeAV(this) => this.len(), + Self::UniqueDirectSumTag(this) => this.len(), Self::UniqueDirectU8(this) | Self::UniqueDirectU16(this) | Self::UniqueDirectU32(this) @@ -874,7 +981,7 @@ impl TypedIndex { fn num_keys(&self) -> usize { match self { Self::BtreeBool(this) => this.num_keys(), - Self::BtreeU8(this) => this.num_keys(), + Self::BtreeU8(this) | Self::BtreeSumTag(this) => this.num_keys(), Self::BtreeI8(this) => this.num_keys(), Self::BtreeU16(this) => this.num_keys(), Self::BtreeI16(this) => this.num_keys(), @@ -890,7 +997,7 @@ impl TypedIndex { Self::BtreeAV(this) => this.num_keys(), Self::UniqueBtreeBool(this) => this.num_keys(), - Self::UniqueBtreeU8(this) => this.num_keys(), + Self::UniqueBtreeU8(this) | Self::UniqueBtreeSumTag(this) => this.num_keys(), Self::UniqueBtreeI8(this) => this.num_keys(), Self::UniqueBtreeU16(this) => this.num_keys(), Self::UniqueBtreeI16(this) => this.num_keys(), @@ -905,6 +1012,7 @@ impl TypedIndex { Self::UniqueBtreeString(this) => this.num_keys(), Self::UniqueBtreeAV(this) => this.num_keys(), + Self::UniqueDirectSumTag(this) => this.num_keys(), Self::UniqueDirectU8(this) | Self::UniqueDirectU16(this) | Self::UniqueDirectU32(this) diff --git a/crates/table/src/table_index/unique_direct_fixed_cap_index.rs b/crates/table/src/table_index/unique_direct_fixed_cap_index.rs new file mode 100644 index 00000000000..e00dac08da5 --- /dev/null +++ b/crates/table/src/table_index/unique_direct_fixed_cap_index.rs @@ -0,0 +1,208 @@ +use super::unique_direct_index::{UniqueDirectIndexPointIter, NONE_PTR}; +use crate::indexes::RowPointer; +use crate::MemoryUsage; +use core::mem; +use core::ops::{Bound, RangeBounds}; +use core::slice::Iter; + +/// A direct index with for relating unsigned integer keys to [`RowPointer`]. +/// The index is provided a capacity on creation and will have that during its lifetime. +/// +/// These indices are intended for small fixed capacities +/// and will be efficient for both monotonic and random insert patterns for small capacities. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UniqueDirectFixedCapIndex { + /// The array holding the elements. + array: Box<[RowPointer]>, + /// The number of keys indexed. + len: usize, +} + +impl MemoryUsage for UniqueDirectFixedCapIndex { + fn heap_usage(&self) -> usize { + let Self { array, len } = self; + array.heap_usage() + len.heap_usage() + } +} + +impl UniqueDirectFixedCapIndex { + /// Returns a new fixed capacity index. + pub fn new(cap: usize) -> Self { + Self { + len: 0, + array: vec![NONE_PTR; cap].into(), + } + } + + /// Clones the structure of the index and returns one with the same capacity. + pub fn clone_structure(&self) -> Self { + Self::new(self.array.len()) + } + + /// Inserts the relation `key -> val` to this index. + /// + /// If `key` was already present in the index, does not add an association with `val`. + /// Returns the existing associated value instead. + /// + /// Panics if the key is beyond the fixed capacity of this index. + pub fn insert(&mut self, key: usize, val: RowPointer) -> Result<(), RowPointer> { + // Fetch the slot. + let slot = &mut self.array[key]; + let in_slot = *slot; + if in_slot == NONE_PTR { + // We have `NONE_PTR`, so not set yet. + *slot = val.with_reserved_bit(true); + self.len += 1; + Ok(()) + } else { + Err(in_slot.with_reserved_bit(false)) + } + } + + /// Deletes `key` from this map. + /// + /// Returns whether `key` was present. + pub fn delete(&mut self, key: usize) -> bool { + let Some(slot) = self.array.get_mut(key) else { + return false; + }; + let old_val = mem::replace(slot, NONE_PTR); + let deleted = old_val != NONE_PTR; + self.len -= deleted as usize; + deleted + } + + /// Returns an iterator yielding the potential [`RowPointer`] for `key`. + pub fn seek_point(&self, key: usize) -> UniqueDirectIndexPointIter { + let point = self.array.get(key).copied().filter(|slot| *slot != NONE_PTR); + UniqueDirectIndexPointIter::new(point) + } + + /// Returns an iterator yielding all the [`RowPointer`] that correspond to the provided `range`. + pub fn seek_range(&self, range: &impl RangeBounds) -> UniqueDirectFixedCapIndexRangeIter { + // Translate `range` to `start..end`. + let end = match range.end_bound() { + Bound::Included(&e) => e + 1, + Bound::Excluded(&e) => e, + Bound::Unbounded => self.array.len(), + }; + let start = match range.start_bound() { + Bound::Included(&s) => s, + Bound::Excluded(&s) => s + 1, + Bound::Unbounded => 0, + }; + + // Normalize `start` so that `start <= end`. + let start = start.min(end); + + // Make the iterator. + UniqueDirectFixedCapIndexRangeIter::new(self.array.get(start..end).unwrap_or_default()) + } + + /// Returns the number of unique keys in the index. + pub fn num_keys(&self) -> usize { + self.len + } + + /// Returns the total number of entries in the index. + pub fn len(&self) -> usize { + self.len + } + + /// Returns whether there are any entries in the index. + #[allow(unused)] // No use for this currently. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Deletes all entries from the index, leaving it empty. + pub fn clear(&mut self) { + self.array.fill(NONE_PTR); + self.len = 0; + } +} + +/// An iterator over a range of keys in a [`UniqueDirectFixedCapIndex`]. +#[derive(Debug)] +pub struct UniqueDirectFixedCapIndexRangeIter<'a> { + iter: Iter<'a, RowPointer>, +} + +impl<'a> UniqueDirectFixedCapIndexRangeIter<'a> { + fn new(slice: &'a [RowPointer]) -> Self { + let iter = slice.iter(); + Self { iter } + } +} + +impl Iterator for UniqueDirectFixedCapIndexRangeIter<'_> { + type Item = RowPointer; + fn next(&mut self) -> Option { + self.iter + // Make sure the row exists. + .find(|slot| **slot != NONE_PTR) + .map(|ptr| ptr.with_reserved_bit(false)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::table_index::unique_direct_index::test::gen_row_pointers; + use core::ops::Range; + use proptest::prelude::*; + + fn range(start: u8, end: u8) -> Range { + let min = start.min(end); + let max = start.max(end); + min as usize..max as usize + } + + fn setup(start: u8, end: u8) -> (UniqueDirectFixedCapIndex, Range, Vec) { + let range = range(start, end); + let (keys, ptrs): (Vec<_>, Vec<_>) = range.clone().zip(gen_row_pointers()).unzip(); + + let mut index = UniqueDirectFixedCapIndex::new(u8::MAX as usize + 1); + for (key, ptr) in keys.iter().zip(&ptrs) { + index.insert(*key, *ptr).unwrap(); + } + assert_eq!(index.len(), range.end - range.start); + (index, range, ptrs) + } + + proptest! { + #[test] + fn seek_range_gives_back_inserted(start: u8, end: u8) { + let (index, range, ptrs) = setup(start, end); + let ptrs_found = index.seek_range(&range).collect::>(); + assert_eq!(ptrs, ptrs_found); + } + + #[test] + fn inserting_again_errors(start: u8, end: u8) { + let (mut index, keys, ptrs) = setup(start, end); + for (key, ptr) in keys.zip(&ptrs) { + assert_eq!(index.insert(key, *ptr).unwrap_err(), *ptr) + } + } + + #[test] + fn deleting_allows_reinsertion(start: u8, end: u8, key: u8) { + let (mut index, range, _) = setup(start, end); + + if range.start == range.end { + return Err(TestCaseError::Reject("empty range".into())); + } + + let key = (key as usize).clamp(range.start, range.end.saturating_sub(1)); + + let ptr = index.seek_point(key).next().unwrap(); + assert!(index.delete(key)); + assert!(!index.delete(key)); + assert_eq!(index.len(), range.end - range.start - 1); + + index.insert(key, ptr).unwrap(); + assert_eq!(index.len(), range.end - range.start); + } + } +} diff --git a/crates/table/src/table_index/unique_direct_index.rs b/crates/table/src/table_index/unique_direct_index.rs index a560afed306..0f30df23170 100644 --- a/crates/table/src/table_index/unique_direct_index.rs +++ b/crates/table/src/table_index/unique_direct_index.rs @@ -46,7 +46,7 @@ impl MemoryUsage for InnerIndex { /// The sentinel used to represent an empty slot in the index. /// The reserved bit set to `false` is used to indicate absence. -const NONE_PTR: RowPointer = RowPointer::new(false, PageIndex(0), PageOffset(0), SquashedOffset::TX_STATE); +pub(super) const NONE_PTR: RowPointer = RowPointer::new(false, PageIndex(0), PageOffset(0), SquashedOffset::TX_STATE); struct InnerIndexKey(usize); @@ -141,19 +141,22 @@ impl UniqueDirectIndex { /// Returns an iterator yielding the potential [`RowPointer`] for `key`. pub fn seek_point(&self, key: usize) -> UniqueDirectIndexPointIter { let (outer_key, inner_key) = split_key(key); - let iter = self + let point = self .outer .get(outer_key) .and_then(|x| x.as_ref()) .map(|inner| inner.get(inner_key)) - .filter(|slot| *slot != NONE_PTR) - .map(|ptr| ptr.with_reserved_bit(false)) - .into_iter(); - UniqueDirectIndexPointIter { iter } + .filter(|slot| *slot != NONE_PTR); + UniqueDirectIndexPointIter::new(point) } /// Returns an iterator yielding all the [`RowPointer`] that correspond to the provided `range`. pub fn seek_range(&self, range: &impl RangeBounds) -> UniqueDirectIndexRangeIter { + // The upper bound of possible key. + // This isn't necessarily the real max key actually present in the index, + // due to possible deletions. + let max_key = self.outer.len() * KEYS_PER_INNER; + // Translate `range` to `start..end`. let start = match range.start_bound() { Bound::Included(&s) => s, @@ -163,14 +166,9 @@ impl UniqueDirectIndex { let end = match range.end_bound() { Bound::Included(&e) => e + 1, // If this wraps, we will clamp to `max_key` later. Bound::Excluded(&e) => e, - Bound::Unbounded => self.len, + Bound::Unbounded => max_key, }; - // The upper bound of possible key. - // This isn't necessarily the real max key actually present in the index, - // due to possible deletions. - let max_key = self.outer.len() * KEYS_PER_INNER; - // Clamp `end` to max possible key in index. let end = end.min(max_key); @@ -213,6 +211,13 @@ pub struct UniqueDirectIndexPointIter { iter: IntoIter, } +impl UniqueDirectIndexPointIter { + pub(super) fn new(point: Option) -> Self { + let iter = point.map(|ptr| ptr.with_reserved_bit(false)).into_iter(); + Self { iter } + } +} + impl Iterator for UniqueDirectIndexPointIter { type Item = RowPointer; fn next(&mut self) -> Option { @@ -265,7 +270,7 @@ impl Iterator for UniqueDirectIndexRangeIter<'_> { } #[cfg(test)] -mod test { +pub(super) mod test { use core::iter::repeat_with; use super::*; @@ -273,7 +278,7 @@ mod test { const FIXED_ROW_SIZE: Size = Size(4 * 4); - fn gen_row_pointers() -> impl Iterator { + pub(crate) fn gen_row_pointers() -> impl Iterator { let mut page_index = PageIndex(0); let mut page_offset = PageOffset(0); repeat_with(move || { diff --git a/modules/sdk-test-cs/Lib.cs b/modules/sdk-test-cs/Lib.cs index 4d933024361..7ece58f476a 100644 --- a/modules/sdk-test-cs/Lib.cs +++ b/modules/sdk-test-cs/Lib.cs @@ -1684,6 +1684,28 @@ public static void delete_pk_connection_id(ReducerContext ctx, ConnectionId a) ctx.Db.pk_connection_id.a.Delete(a); } + [SpacetimeDB.Table(Name = "pk_simple_enum", Public = true)] + public partial struct PkSimpleEnum + { + [SpacetimeDB.PrimaryKey] + public SimpleEnum a; + public int data; + } + + [SpacetimeDB.Reducer] + public static void insert_pk_simple_enum(ReducerContext ctx, SimpleEnum a, int data) + { + ctx.Db.pk_simple_enum.Insert(new PkSimpleEnum { a = a, data = data }); + } + + [SpacetimeDB.Reducer] + public static void update_pk_simple_enum(ReducerContext ctx, SimpleEnum a, int data) + { + var o = ctx.Db.pk_simple_enum.a.Find(a) ?? throw new ArgumentException("key not found"); + o.data = data; + ctx.Db.pk_simple_enum.a.Update(o); + } + [SpacetimeDB.Reducer] public static void insert_caller_one_identity(ReducerContext ctx) { @@ -2025,4 +2047,26 @@ public static void insert_user(ReducerContext ctx, string name, Identity identit { ctx.Db.users.Insert(new Users { name = name, identity = identity }); } + + [SpacetimeDB.Table(Name = "indexed_simple_enum", Public = true)] + public partial struct IndexedSimpleEnum { + [SpacetimeDB.Index.BTree] + public SimpleEnum n; + } + + [SpacetimeDB.Reducer] + public static void insert_into_indexed_simple_enum(ReducerContext ctx, SimpleEnum n) + { + ctx.Db.indexed_simple_enum.Insert(new IndexedSimpleEnum { n = n }); + } + + [SpacetimeDB.Reducer] + public static void update_indexed_simple_enum(ReducerContext ctx, SimpleEnum a, SimpleEnum b) + { + foreach (var item in ctx.Db.indexed_simple_enum.n.Filter(a)) + { + ctx.Db.indexed_simple_enum.n.Delete(a); + ctx.Db.indexed_simple_enum.Insert(new IndexedSimpleEnum { n = b }); + } + } } diff --git a/modules/sdk-test/src/lib.rs b/modules/sdk-test/src/lib.rs index 4dbfffe49fa..7398288bdae 100644 --- a/modules/sdk-test/src/lib.rs +++ b/modules/sdk-test/src/lib.rs @@ -6,13 +6,13 @@ // and clippy misunderstands `#[allow]` attributes in macro-expansions. #![allow(clippy::too_many_arguments)] -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use spacetimedb::{ sats::{i256, u256}, ConnectionId, Identity, ReducerContext, SpacetimeType, Table, TimeDuration, Timestamp, }; -#[derive(SpacetimeType)] +#[derive(PartialEq, Eq, Hash, SpacetimeType)] pub enum SimpleEnum { Zero, One, @@ -542,6 +542,20 @@ define_tables! { update_by update_pk_connection_id = update_by_a(a), delete_by delete_pk_connection_id = delete_by_a(a: ConnectionId), } #[primary_key] a ConnectionId, data i32; + + PkSimpleEnum { + insert_or_panic insert_pk_simple_enum, + } #[primary_key] a SimpleEnum, data i32; +} + +#[spacetimedb::reducer] +fn update_pk_simple_enum(ctx: &ReducerContext, a: SimpleEnum, data: i32) -> anyhow::Result<()> { + let Some(mut o) = ctx.db.pk_simple_enum().a().find(a) else { + return Err(anyhow!("row not found")); + }; + o.data = data; + ctx.db.pk_simple_enum().a().update(o); + Ok(()) } #[spacetimedb::reducer] @@ -781,3 +795,24 @@ fn insert_user(ctx: &ReducerContext, name: String, identity: Identity) -> anyhow ctx.db.users().insert(Users { name, identity }); Ok(()) } + +#[spacetimedb::table(name = indexed_simple_enum, public)] +struct IndexedSimpleEnum { + #[index(btree)] + n: SimpleEnum, +} + +#[spacetimedb::reducer] +fn insert_into_indexed_simple_enum(ctx: &ReducerContext, n: SimpleEnum) -> anyhow::Result<()> { + ctx.db.indexed_simple_enum().insert(IndexedSimpleEnum { n }); + Ok(()) +} + +#[spacetimedb::reducer] +fn update_indexed_simple_enum(ctx: &ReducerContext, a: SimpleEnum, b: SimpleEnum) -> anyhow::Result<()> { + if ctx.db.indexed_simple_enum().n().filter(&a).next().is_some() { + ctx.db.indexed_simple_enum().n().delete(&a); + ctx.db.indexed_simple_enum().insert(IndexedSimpleEnum { n: b }); + } + Ok(()) +}