diff --git a/crates/bindings-csharp/BSATN.Runtime.Tests/Tests.cs b/crates/bindings-csharp/BSATN.Runtime.Tests/Tests.cs index c22879a596c..eb82b5f61c4 100644 --- a/crates/bindings-csharp/BSATN.Runtime.Tests/Tests.cs +++ b/crates/bindings-csharp/BSATN.Runtime.Tests/Tests.cs @@ -165,18 +165,26 @@ public static void TimestampConversionChecks() var newIntervalUs = 333L; var newInterval = new TimeDuration(newIntervalUs); + var laterInterval = new TimeDuration(newIntervalUs + 1); var laterStamp = stamp + newInterval; Assert.Equal(laterStamp.MicrosecondsSinceUnixEpoch, us + newIntervalUs); Assert.Equal(laterStamp.TimeDurationSince(stamp), newInterval); #pragma warning disable CS1718 Assert.True(stamp == stamp); + Assert.True(newInterval == newInterval); #pragma warning restore CS1718 Assert.False(stamp == laterStamp); Assert.True(stamp < laterStamp); Assert.False(laterStamp < stamp); Assert.Equal(-1, stamp.CompareTo(laterStamp)); Assert.Equal(+1, laterStamp.CompareTo(stamp)); + + Assert.False(newInterval == laterInterval); + Assert.True(newInterval < laterInterval); + Assert.False(laterInterval < newInterval); + Assert.Equal(-1, newInterval.CompareTo(laterInterval)); + Assert.Equal(+1, laterInterval.CompareTo(newInterval)); } [Fact] diff --git a/crates/bindings-csharp/BSATN.Runtime/Builtins.cs b/crates/bindings-csharp/BSATN.Runtime/Builtins.cs index f7122db477e..669bab9ad77 100644 --- a/crates/bindings-csharp/BSATN.Runtime/Builtins.cs +++ b/crates/bindings-csharp/BSATN.Runtime/Builtins.cs @@ -365,7 +365,7 @@ public readonly TimeDuration TimeDurationSince(Timestamp earlier) => public static Timestamp operator -(Timestamp point, TimeDuration interval) => new Timestamp(checked(point.MicrosecondsSinceUnixEpoch - interval.Microseconds)); - public int CompareTo(Timestamp that) + public readonly int CompareTo(Timestamp that) { return this.MicrosecondsSinceUnixEpoch.CompareTo(that.MicrosecondsSinceUnixEpoch); } @@ -428,7 +428,9 @@ public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) => /// This type has less precision than TimeSpan (units of microseconds rather than units of 100ns). /// [StructLayout(LayoutKind.Sequential)] -public record struct TimeDuration(long Microseconds) : IStructuralReadWrite +public record struct TimeDuration(long Microseconds) + : IStructuralReadWrite, + IComparable { public static readonly TimeDuration ZERO = new(0); @@ -457,6 +459,22 @@ public override readonly string ToString() return $"{sign}{secs}.{microsRemaining:D6}"; } + /// + public readonly int CompareTo(TimeDuration that) + { + return this.Microseconds.CompareTo(that.Microseconds); + } + + public static bool operator <(TimeDuration l, TimeDuration r) + { + return l.CompareTo(r) == -1; + } + + public static bool operator >(TimeDuration l, TimeDuration r) + { + return l.CompareTo(r) == 1; + } + // --- auto-generated --- public void ReadFields(BinaryReader reader) { diff --git a/crates/bindings/tests/ui/tables.stderr b/crates/bindings/tests/ui/tables.stderr index d003ce5faca..17238040125 100644 --- a/crates/bindings/tests/ui/tables.stderr +++ b/crates/bindings/tests/ui/tables.stderr @@ -132,8 +132,8 @@ error[E0277]: `&'a Alpha` cannot appear as an argument to an index filtering ope &RawMiscModuleExportV9 &TableAccess &TableType - &bool - ðnum::int::I256 + &TimeDuration + &[u8] and $N others note: required by a bound in `UniqueColumn::::ColType, Col>::find` --> src/table.rs @@ -159,8 +159,8 @@ error[E0277]: the trait bound `Alpha: IndexScanRangeBounds<(Alpha,), SingleBound &RawMiscModuleExportV9 &TableAccess &TableType - &bool - ðnum::int::I256 + &TimeDuration + &[u8] and $N others = note: required for `Alpha` to implement `IndexScanRangeBounds<(Alpha,), SingleBound>` note: required by a bound in `RangedIndex::::filter` diff --git a/crates/codegen/tests/snapshots/codegen__codegen_csharp.snap b/crates/codegen/tests/snapshots/codegen__codegen_csharp.snap index 2a438d3c806..7f8e7d09506 100644 --- a/crates/codegen/tests/snapshots/codegen__codegen_csharp.snap +++ b/crates/codegen/tests/snapshots/codegen__codegen_csharp.snap @@ -1035,6 +1035,7 @@ namespace SpacetimeDB { public RemoteTables(DbConnection conn) { + AddTable(Filterable = new(conn)); AddTable(HasSpecialStuff = new(conn)); AddTable(LoggedOutPlayer = new(conn)); AddTable(Person = new(conn)); @@ -1538,6 +1539,107 @@ namespace SpacetimeDB } } ''' +"Tables/Filterable.g.cs" = ''' +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +VERSION_COMMENT + +#nullable enable + +using System; +using SpacetimeDB.BSATN; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB +{ + public sealed partial class RemoteTables + { + public sealed class FilterableHandle : RemoteTableHandle + { + protected override string RemoteTableName => "filterable"; + + public sealed class BlobIndex : BTreeIndexBase> + { + protected override System.Collections.Generic.List GetKey(Filterable row) => row.Blob; + + public BlobIndex(FilterableHandle table) : base(table) { } + } + + public readonly BlobIndex Blob; + + public sealed class ConnectionIdIndex : BTreeIndexBase + { + protected override SpacetimeDB.ConnectionId GetKey(Filterable row) => row.ConnectionId; + + public ConnectionIdIndex(FilterableHandle table) : base(table) { } + } + + public readonly ConnectionIdIndex ConnectionId; + + public sealed class IdentityIndex : BTreeIndexBase + { + protected override SpacetimeDB.Identity GetKey(Filterable row) => row.Identity; + + public IdentityIndex(FilterableHandle table) : base(table) { } + } + + public readonly IdentityIndex Identity; + + public sealed class StrIndex : BTreeIndexBase + { + protected override string GetKey(Filterable row) => row.Str; + + public StrIndex(FilterableHandle table) : base(table) { } + } + + public readonly StrIndex Str; + + public sealed class TestCIndex : BTreeIndexBase + { + protected override NamespaceTestC GetKey(Filterable row) => row.TestC; + + public TestCIndex(FilterableHandle table) : base(table) { } + } + + public readonly TestCIndex TestC; + + public sealed class TimeDurationIndex : BTreeIndexBase + { + protected override SpacetimeDB.TimeDuration GetKey(Filterable row) => row.TimeDuration; + + public TimeDurationIndex(FilterableHandle table) : base(table) { } + } + + public readonly TimeDurationIndex TimeDuration; + + public sealed class TimestampIndex : BTreeIndexBase + { + protected override SpacetimeDB.Timestamp GetKey(Filterable row) => row.Timestamp; + + public TimestampIndex(FilterableHandle table) : base(table) { } + } + + public readonly TimestampIndex Timestamp; + + internal FilterableHandle(DbConnection conn) : base(conn) + { + Blob = new(this); + ConnectionId = new(this); + Identity = new(this); + Str = new(this); + TestC = new(this); + TimeDuration = new(this); + Timestamp = new(this); + } + } + + public readonly FilterableHandle Filterable; + } +} +''' "Tables/HasSpecialStuff.g.cs" = ''' // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. @@ -2105,6 +2207,66 @@ namespace SpacetimeDB } } ''' +"Types/Filterable.g.cs" = ''' +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +VERSION_COMMENT + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB +{ + [SpacetimeDB.Type] + [DataContract] + public sealed partial class Filterable + { + [DataMember(Name = "str")] + public string Str; + [DataMember(Name = "identity")] + public SpacetimeDB.Identity Identity; + [DataMember(Name = "connection_id")] + public SpacetimeDB.ConnectionId ConnectionId; + [DataMember(Name = "test_c")] + public NamespaceTestC TestC; + [DataMember(Name = "timestamp")] + public SpacetimeDB.Timestamp Timestamp; + [DataMember(Name = "time_duration")] + public SpacetimeDB.TimeDuration TimeDuration; + [DataMember(Name = "blob")] + public System.Collections.Generic.List Blob; + + public Filterable( + string Str, + SpacetimeDB.Identity Identity, + SpacetimeDB.ConnectionId ConnectionId, + NamespaceTestC TestC, + SpacetimeDB.Timestamp Timestamp, + SpacetimeDB.TimeDuration TimeDuration, + System.Collections.Generic.List Blob + ) + { + this.Str = Str; + this.Identity = Identity; + this.ConnectionId = ConnectionId; + this.TestC = TestC; + this.Timestamp = Timestamp; + this.TimeDuration = TimeDuration; + this.Blob = Blob; + } + + public Filterable() + { + this.Str = ""; + this.Blob = new(); + } + } +} +''' "Types/Foobar.g.cs" = ''' // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. diff --git a/crates/codegen/tests/snapshots/codegen__codegen_rust.snap b/crates/codegen/tests/snapshots/codegen__codegen_rust.snap index a9b3076f388..0bcc0715b5c 100644 --- a/crates/codegen/tests/snapshots/codegen__codegen_rust.snap +++ b/crates/codegen/tests/snapshots/codegen__codegen_rust.snap @@ -799,6 +799,143 @@ impl set_flags_for_delete_players_by_name for super::SetReducerFlags { } } +''' +"filterable_table.rs" = ''' +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +VERSION_COMMENT + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; +use super::filterable_type::Filterable; +use super::namespace_test_c_type::NamespaceTestC; + +/// Table handle for the table `filterable`. +/// +/// Obtain a handle from the [`FilterableTableAccess::filterable`] method on [`super::RemoteTables`], +/// like `ctx.db.filterable()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.filterable().on_insert(...)`. +pub struct FilterableTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `filterable`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait FilterableTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`FilterableTableHandle`], which mediates access to the table `filterable`. + fn filterable(&self) -> FilterableTableHandle<'_>; +} + +impl FilterableTableAccess for super::RemoteTables { + fn filterable(&self) -> FilterableTableHandle<'_> { + FilterableTableHandle { + imp: self.imp.get_table::("filterable"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct FilterableInsertCallbackId(__sdk::CallbackId); +pub struct FilterableDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for FilterableTableHandle<'ctx> { + type Row = Filterable; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { self.imp.count() } + fn iter(&self) -> impl Iterator + '_ { self.imp.iter() } + + type InsertCallbackId = FilterableInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> FilterableInsertCallbackId { + FilterableInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: FilterableInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = FilterableDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> FilterableDeleteCallbackId { + FilterableDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: FilterableDeleteCallbackId) { + 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::("filterable"); +} + +#[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() + }) +} +''' +"filterable_type.rs" = ''' +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +VERSION_COMMENT + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + +use super::namespace_test_c_type::NamespaceTestC; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Filterable { + pub str: String, + pub identity: __sdk::Identity, + pub connection_id: __sdk::ConnectionId, + pub test_c: NamespaceTestC, + pub timestamp: __sdk::Timestamp, + pub time_duration: __sdk::TimeDuration, + pub blob: Vec::, +} + + +impl __sdk::InModule for Filterable { + type Module = super::RemoteModule; +} + ''' "foobar_type.rs" = ''' // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE @@ -1408,6 +1545,7 @@ use spacetimedb_sdk::__codegen::{ }; pub mod baz_type; +pub mod filterable_type; pub mod foobar_type; pub mod has_special_stuff_type; pub mod person_type; @@ -1437,6 +1575,7 @@ pub mod repeating_test_reducer; pub mod say_hello_reducer; pub mod test_reducer; pub mod test_btree_index_args_reducer; +pub mod filterable_table; pub mod has_special_stuff_table; pub mod logged_out_player_table; pub mod person_table; @@ -1451,6 +1590,7 @@ pub mod test_e_table; pub mod test_f_table; pub use baz_type::Baz; +pub use filterable_type::Filterable; pub use foobar_type::Foobar; pub use has_special_stuff_type::HasSpecialStuff; pub use person_type::Person; @@ -1466,6 +1606,7 @@ pub use test_e_type::TestE; pub use test_foobar_type::TestFoobar; pub use namespace_test_c_type::NamespaceTestC; pub use namespace_test_f_type::NamespaceTestF; +pub use filterable_table::*; pub use has_special_stuff_table::*; pub use logged_out_player_table::*; pub use person_table::*; @@ -1589,7 +1730,8 @@ fn try_from(value: __ws::ReducerCallInfo<__ws::BsatnFormat>) -> __sdk::Result, + filterable: __sdk::TableUpdate, + has_special_stuff: __sdk::TableUpdate, logged_out_player: __sdk::TableUpdate, person: __sdk::TableUpdate, pk_multi_identity: __sdk::TableUpdate, @@ -1611,7 +1753,8 @@ impl TryFrom<__ws::DatabaseUpdate<__ws::BsatnFormat>> for DbUpdate { for table_update in raw.tables { match &table_update.table_name[..] { - "has_special_stuff" => db_update.has_special_stuff.append(has_special_stuff_table::parse_table_update(table_update)?), + "filterable" => db_update.filterable.append(filterable_table::parse_table_update(table_update)?), + "has_special_stuff" => db_update.has_special_stuff.append(has_special_stuff_table::parse_table_update(table_update)?), "logged_out_player" => db_update.logged_out_player.append(logged_out_player_table::parse_table_update(table_update)?), "person" => db_update.person.append(person_table::parse_table_update(table_update)?), "pk_multi_identity" => db_update.pk_multi_identity.append(pk_multi_identity_table::parse_table_update(table_update)?), @@ -1645,7 +1788,8 @@ impl __sdk::DbUpdate for DbUpdate { fn apply_to_client_cache(&self, cache: &mut __sdk::ClientCache) -> AppliedDiff<'_> { let mut diff = AppliedDiff::default(); - diff.has_special_stuff = cache.apply_diff_to_table::("has_special_stuff", &self.has_special_stuff); + diff.filterable = cache.apply_diff_to_table::("filterable", &self.filterable); + diff.has_special_stuff = cache.apply_diff_to_table::("has_special_stuff", &self.has_special_stuff); diff.logged_out_player = cache.apply_diff_to_table::("logged_out_player", &self.logged_out_player).with_updates_by_pk(|row| &row.identity); diff.person = cache.apply_diff_to_table::("person", &self.person).with_updates_by_pk(|row| &row.id); diff.pk_multi_identity = cache.apply_diff_to_table::("pk_multi_identity", &self.pk_multi_identity).with_updates_by_pk(|row| &row.id); @@ -1666,7 +1810,8 @@ impl __sdk::DbUpdate for DbUpdate { #[allow(non_snake_case)] #[doc(hidden)] pub struct AppliedDiff<'r> { - has_special_stuff: __sdk::TableAppliedDiff<'r, HasSpecialStuff>, + filterable: __sdk::TableAppliedDiff<'r, Filterable>, + has_special_stuff: __sdk::TableAppliedDiff<'r, HasSpecialStuff>, logged_out_player: __sdk::TableAppliedDiff<'r, Player>, person: __sdk::TableAppliedDiff<'r, Person>, pk_multi_identity: __sdk::TableAppliedDiff<'r, PkMultiIdentity>, @@ -1687,7 +1832,8 @@ 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::("has_special_stuff", &self.has_special_stuff, event); + callbacks.invoke_table_row_callbacks::("filterable", &self.filterable, event); + callbacks.invoke_table_row_callbacks::("has_special_stuff", &self.has_special_stuff, event); callbacks.invoke_table_row_callbacks::("logged_out_player", &self.logged_out_player, event); callbacks.invoke_table_row_callbacks::("person", &self.person, event); callbacks.invoke_table_row_callbacks::("pk_multi_identity", &self.pk_multi_identity, event); @@ -2286,7 +2432,8 @@ impl __sdk::SpacetimeModule for RemoteModule { type SubscriptionHandle = SubscriptionHandle; fn register_tables(client_cache: &mut __sdk::ClientCache) { - has_special_stuff_table::register_table(client_cache); + filterable_table::register_table(client_cache); + has_special_stuff_table::register_table(client_cache); logged_out_player_table::register_table(client_cache); person_table::register_table(client_cache); pk_multi_identity_table::register_table(client_cache); @@ -4474,9 +4621,9 @@ use spacetimedb_sdk::__codegen::{ __ws, }; +use super::namespace_test_c_type::NamespaceTestC; use super::test_a_type::TestA; use super::test_b_type::TestB; -use super::namespace_test_c_type::NamespaceTestC; use super::namespace_test_f_type::NamespaceTestF; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] diff --git a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap index 43caad9b21d..b5f13df4aaa 100644 --- a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap +++ b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap @@ -517,6 +517,167 @@ export namespace DeletePlayersByName { } +''' +"filterable_table.ts" = ''' +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +VERSION_COMMENT + +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +import { + AlgebraicType, + AlgebraicValue, + BinaryReader, + BinaryWriter, + ConnectionId, + DbConnectionBuilder, + DbConnectionImpl, + Identity, + ProductType, + ProductTypeElement, + SubscriptionBuilderImpl, + SumType, + SumTypeVariant, + TableCache, + TimeDuration, + Timestamp, + deepEqual, + type CallReducerFlags, + type DbContext, + type ErrorContextInterface, + type Event, + type EventContextInterface, + type ReducerEventContextInterface, + type SubscriptionEventContextInterface, +} from "@clockworklabs/spacetimedb-sdk"; +import { Filterable } from "./filterable_type"; +import { NamespaceTestC as __NamespaceTestC } from "./namespace_test_c_type"; + +import { type EventContext, type Reducer, RemoteReducers, RemoteTables } from "."; + +/** + * Table handle for the table `filterable`. + * + * Obtain a handle from the [`filterable`] property on [`RemoteTables`], + * like `ctx.db.filterable`. + * + * Users are encouraged not to explicitly reference this type, + * but to directly chain method calls, + * like `ctx.db.filterable.on_insert(...)`. + */ +export class FilterableTableHandle { + tableCache: TableCache; + + constructor(tableCache: TableCache) { + this.tableCache = tableCache; + } + + count(): number { + return this.tableCache.count(); + } + + iter(): Iterable { + return this.tableCache.iter(); + } + + onInsert = (cb: (ctx: EventContext, row: Filterable) => void) => { + return this.tableCache.onInsert(cb); + } + + removeOnInsert = (cb: (ctx: EventContext, row: Filterable) => void) => { + return this.tableCache.removeOnInsert(cb); + } + + onDelete = (cb: (ctx: EventContext, row: Filterable) => void) => { + return this.tableCache.onDelete(cb); + } + + removeOnDelete = (cb: (ctx: EventContext, row: Filterable) => void) => { + return this.tableCache.removeOnDelete(cb); + } +} +''' +"filterable_type.ts" = ''' +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +VERSION_COMMENT + +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +import { + AlgebraicType, + AlgebraicValue, + BinaryReader, + BinaryWriter, + ConnectionId, + DbConnectionBuilder, + DbConnectionImpl, + Identity, + ProductType, + ProductTypeElement, + SubscriptionBuilderImpl, + SumType, + SumTypeVariant, + TableCache, + TimeDuration, + Timestamp, + deepEqual, + type CallReducerFlags, + type DbContext, + type ErrorContextInterface, + type Event, + type EventContextInterface, + type ReducerEventContextInterface, + type SubscriptionEventContextInterface, +} from "@clockworklabs/spacetimedb-sdk"; +import { NamespaceTestC as __NamespaceTestC } from "./namespace_test_c_type"; + +export type Filterable = { + str: string, + identity: Identity, + connectionId: ConnectionId, + testC: __NamespaceTestC, + timestamp: Timestamp, + timeDuration: TimeDuration, + blob: Uint8Array, +}; + +/** + * A namespace for generated helper functions. + */ +export namespace Filterable { + /** + * A function which returns this type represented as an AlgebraicType. + * This function is derived from the AlgebraicType used to generate this type. + */ + export function getTypeScriptAlgebraicType(): AlgebraicType { + return AlgebraicType.createProductType([ + new ProductTypeElement("str", AlgebraicType.createStringType()), + new ProductTypeElement("identity", AlgebraicType.createIdentityType()), + new ProductTypeElement("connectionId", AlgebraicType.createConnectionIdType()), + new ProductTypeElement("testC", __NamespaceTestC.getTypeScriptAlgebraicType()), + new ProductTypeElement("timestamp", AlgebraicType.createTimestampType()), + new ProductTypeElement("timeDuration", AlgebraicType.createTimeDurationType()), + new ProductTypeElement("blob", AlgebraicType.createArrayType(AlgebraicType.createU8Type())), + ]); + } + + export function serialize(writer: BinaryWriter, value: Filterable): void { + Filterable.getTypeScriptAlgebraicType().serialize(writer, value); + } + + export function deserialize(reader: BinaryReader): Filterable { + return Filterable.getTypeScriptAlgebraicType().deserialize(reader); + } + +} + + ''' "foobar_type.ts" = ''' // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE @@ -812,6 +973,8 @@ import { TestBtreeIndexArgs } from "./test_btree_index_args_reducer.ts"; export { TestBtreeIndexArgs }; // Import and reexport all table handle types +import { FilterableTableHandle } from "./filterable_table.ts"; +export { FilterableTableHandle }; import { HasSpecialStuffTableHandle } from "./has_special_stuff_table.ts"; export { HasSpecialStuffTableHandle }; import { LoggedOutPlayerTableHandle } from "./logged_out_player_table.ts"; @@ -840,6 +1003,8 @@ export { TestFTableHandle }; // Import and reexport all types import { Baz } from "./baz_type.ts"; export { Baz }; +import { Filterable } from "./filterable_type.ts"; +export { Filterable }; import { Foobar } from "./foobar_type.ts"; export { Foobar }; import { HasSpecialStuff } from "./has_special_stuff_type.ts"; @@ -873,6 +1038,10 @@ export { NamespaceTestF }; const REMOTE_MODULE = { tables: { + filterable: { + tableName: "filterable", + rowType: Filterable.getTypeScriptAlgebraicType(), + }, has_special_stuff: { tableName: "has_special_stuff", rowType: HasSpecialStuff.getTypeScriptAlgebraicType(), @@ -1327,6 +1496,10 @@ export class SetReducerFlags { export class RemoteTables { constructor(private connection: DbConnectionImpl) {} + get filterable(): FilterableTableHandle { + return new FilterableTableHandle(this.connection.clientCache.getOrCreateTable(REMOTE_MODULE.tables.filterable)); + } + get hasSpecialStuff(): HasSpecialStuffTableHandle { return new HasSpecialStuffTableHandle(this.connection.clientCache.getOrCreateTable(REMOTE_MODULE.tables.has_special_stuff)); } @@ -3876,9 +4049,9 @@ import { type SubscriptionEventContextInterface, } from "@clockworklabs/spacetimedb-sdk"; +import { NamespaceTestC as __NamespaceTestC } from "./namespace_test_c_type"; import { TestA as __TestA } from "./test_a_type"; import { TestB as __TestB } from "./test_b_type"; -import { NamespaceTestC as __NamespaceTestC } from "./namespace_test_c_type"; import { NamespaceTestF as __NamespaceTestF } from "./namespace_test_f_type"; export type Test = { diff --git a/crates/lib/src/filterable_value.rs b/crates/lib/src/filterable_value.rs index 82a90eb7c4e..9bbff2a3b19 100644 --- a/crates/lib/src/filterable_value.rs +++ b/crates/lib/src/filterable_value.rs @@ -1,6 +1,8 @@ use crate::{ConnectionId, Identity}; use core::ops; use spacetimedb_sats::bsatn; +use spacetimedb_sats::time_duration::TimeDuration; +use spacetimedb_sats::timestamp::Timestamp; use spacetimedb_sats::{hash::Hash, i256, u256, Serialize}; /// Types which can appear as an argument to an index filtering operation @@ -17,6 +19,8 @@ use spacetimedb_sats::{hash::Hash, i256, u256, Serialize}; /// - [`Identity`]. /// - [`ConnectionId`]. /// - [`Hash`](struct@Hash). +/// - [`Timestamp`](struct@Timestamp). +/// - [`TimeDuration`](struct@TimeDuration). /// - No-payload enums annotated with `#[derive(SpacetimeType)]`. /// No-payload enums are sometimes called "plain," "simple" or "C-style." /// They are enums where no variant has any payload data. @@ -97,11 +101,10 @@ impl_filterable_value! { 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, + Timestamp: Copy, + TimeDuration: Copy, + + &[u8] => Vec, } pub enum TermBound { diff --git a/modules/module-test-cs/Lib.cs b/modules/module-test-cs/Lib.cs index 6e0975f48f1..aad6372b674 100644 --- a/modules/module-test-cs/Lib.cs +++ b/modules/module-test-cs/Lib.cs @@ -171,6 +171,27 @@ public partial struct Player public string name; } +// Extra table for checking implementation of `FilterableValue` trait +[Table(Name = "filterable")] +public partial struct Filterable +{ + // Elided the trivial types as `int's, bool`... + [Index.BTree] + public String str; + [Index.BTree] + public Identity identity; + [Index.BTree] + public ConnectionId connection_id; + [Index.BTree] + public TestC test_c; + [Index.BTree] + public Timestamp timestamp; + [Index.BTree] + public TimeDuration time_duration; + [Index.BTree] + public byte[] blob; +} + // ───────────────────────────────────────────────────────────────────────────── // SUPPORT TYPES // ───────────────────────────────────────────────────────────────────────────── @@ -416,6 +437,14 @@ public static void test_btree_index_args(ReducerContext ctx) ctx.Db.test_e.name.Delete(s); ctx.Db.test_e.name.Delete("str"); + // Single-column indexes on `Filterable` table: + var _a1 = ctx.Db.filterable.str.Filter("string"); + var _a2 = ctx.Db.filterable.identity.Filter(Identity.FromHexString("0x0")); + var _a3 = ctx.Db.filterable.test_c.Filter(TestC.Foo); + var _a4 = ctx.Db.filterable.connection_id.Filter(ConnectionId.Random()); + var _a5 = ctx.Db.filterable.timestamp.Filter(new Timestamp(0)); + var _a6 = ctx.Db.filterable.time_duration.Filter(new TimeDuration(0)); + var _a7 = ctx.Db.filterable.blob.Filter([0x01, 0x02, 0x03]); // For the multi‑column index on points, assume the API offers overloads that accept ranges. var mci = ctx.Db.points.multi_column_index; var _a = mci.Filter(0L); diff --git a/modules/module-test/src/lib.rs b/modules/module-test/src/lib.rs index 3d5a512406b..6935675ac6d 100644 --- a/modules/module-test/src/lib.rs +++ b/modules/module-test/src/lib.rs @@ -1,10 +1,10 @@ #![allow(clippy::disallowed_names)] -use spacetimedb::log; use spacetimedb::spacetimedb_lib::db::raw_def::v9::TableAccess; use spacetimedb::spacetimedb_lib::{self, bsatn}; use spacetimedb::{ duration, table, ConnectionId, Deserialize, Identity, ReducerContext, SpacetimeType, Table, Timestamp, }; +use spacetimedb::{log, TimeDuration}; pub type TestAlias = TestA; @@ -161,6 +161,31 @@ pub struct Player { name: String, } +/// Extra table for checking implementation of `FilterableValue`. +/// +/// See [`FilterableValue`](spacetimedb::spacetimedb_lib::FilterableValue) for more details. +#[spacetimedb::table(name = filterable)] +pub struct Filterable { + // Elided the trivial types as `int's, bool`... + #[index(btree)] + str: String, + #[index(btree)] + identity: Identity, + #[index(btree)] + connection_id: ConnectionId, + // Not available in C# yet, and we need to mirror the table definition. + // #[index(btree)] + // hash: Hash, + #[index(btree)] + test_c: TestC, + #[index(btree)] + timestamp: Timestamp, + #[index(btree)] + time_duration: TimeDuration, + #[index(btree)] + blob: Vec, +} + // ───────────────────────────────────────────────────────────────────────────── // SUPPORT TYPES // ───────────────────────────────────────────────────────────────────────────── @@ -368,6 +393,31 @@ fn test_btree_index_args(ctx: &ReducerContext) { // ctx.db.test_e().name().delete(string); // SHOULD FAIL + // Single-column indexes on `Filterable` table: + let _ = ctx.db.filterable().str().filter("string"); + let _ = ctx.db.filterable().identity().filter(Identity::ZERO); + let _ = ctx.db.filterable().identity().filter(&Identity::ZERO); + let _ = ctx.db.filterable().connection_id().filter(ConnectionId::ZERO); + let _ = ctx.db.filterable().connection_id().filter(&ConnectionId::ZERO); + let _ = ctx.db.filterable().test_c().filter(TestC::Foo); + let _ = ctx + .db + .filterable() + .timestamp() + .filter(Timestamp::from_micros_since_unix_epoch(0)); + let _ = ctx + .db + .filterable() + .timestamp() + .filter(&Timestamp::from_micros_since_unix_epoch(0)); + let _ = ctx.db.filterable().time_duration().filter(TimeDuration::from_micros(0)); + let _ = ctx + .db + .filterable() + .time_duration() + .filter(&TimeDuration::from_micros(0)); + let _ = ctx.db.filterable().blob().filter(&[1u8, 2, 3][..]); + // Multi-column i64 index on `points.x, points.y`: // Tests that we can pass various ranges // and various combinations of borrowed/owned `Copy` values. diff --git a/sdks/typescript/packages/sdk/src/time_duration.ts b/sdks/typescript/packages/sdk/src/time_duration.ts index 6179ccd3140..8b825d8797b 100644 --- a/sdks/typescript/packages/sdk/src/time_duration.ts +++ b/sdks/typescript/packages/sdk/src/time_duration.ts @@ -21,4 +21,11 @@ export class TimeDuration { static fromMillis(millis: number): TimeDuration { return new TimeDuration(BigInt(millis) * TimeDuration.MICROS_PER_MILLIS); } + + /** + * Compare two TimeDuration for equality. + */ + isEqual(other: TimeDuration): boolean { + return this.micros === other.micros; + } } diff --git a/sdks/typescript/packages/sdk/src/timestamp.ts b/sdks/typescript/packages/sdk/src/timestamp.ts index 6a18fd131b7..a7db28e5f3a 100644 --- a/sdks/typescript/packages/sdk/src/timestamp.ts +++ b/sdks/typescript/packages/sdk/src/timestamp.ts @@ -54,4 +54,11 @@ export class Timestamp { } return new Date(Number(millis)); } + + /** + * Compare two Timestamp for equality. + */ + isEqual(other: Timestamp): boolean { + return this.microsSinceUnixEpoch === other.microsSinceUnixEpoch; + } } diff --git a/sdks/typescript/packages/sdk/tests/algebraic_value.test.ts b/sdks/typescript/packages/sdk/tests/algebraic_value.test.ts index 12e778223ed..bbc71a4cecb 100644 --- a/sdks/typescript/packages/sdk/tests/algebraic_value.test.ts +++ b/sdks/typescript/packages/sdk/tests/algebraic_value.test.ts @@ -8,6 +8,7 @@ import { } from '../src/algebraic_value'; import BinaryReader from '../src/binary_reader'; import BinaryWriter from '../src/binary_writer'; +import { deepEqual, Identity, TimeDuration, Timestamp } from '../src'; describe('AlgebraicValue', () => { test('when created with a ProductValue it assigns the product property', () => { @@ -166,5 +167,38 @@ describe('AlgebraicValue', () => { expect(result.asString()).toEqual('zażółć gęślą jaźń'); }); + + test('should correctly compare values', () => { + const identity_a = Identity.fromString( + '0000000000000000000000000000000000000000000000000000000000000069' + ); + const identity_b = Identity.fromString( + '0000000000000000000000000000000000000000000000000000000000000067' + ); + + expect(deepEqual(identity_a, identity_a)).toEqual(true); + expect(identity_a.isEqual(identity_a)).toEqual(true); + + expect(deepEqual(identity_a, identity_b)).toEqual(false); + expect(identity_a.isEqual(identity_b)).toEqual(false); + + const time_a = Timestamp.fromDate(new Date(1722500000000)); + const time_b = Timestamp.fromDate(new Date(1722600000000)); + + expect(deepEqual(time_a, time_a)).toEqual(true); + expect(time_a.isEqual(time_a)).toEqual(true); + + expect(deepEqual(time_a, time_b)).toEqual(false); + expect(time_a.isEqual(time_b)).toEqual(false); + + const dur_a = TimeDuration.fromMillis(0); + const dur_b = TimeDuration.fromMillis(1); + + expect(deepEqual(dur_a, dur_a)).toEqual(true); + expect(dur_a.isEqual(dur_a)).toEqual(true); + + expect(deepEqual(dur_a, dur_b)).toEqual(false); + expect(dur_a.isEqual(dur_b)).toEqual(false); + }); }); });