diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/Lib.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/Lib.cs index 8b60cd937bb..f32ee8c85d2 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/Lib.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/Lib.cs @@ -518,7 +518,9 @@ public partial class ViewPrimaryKeyPartialRow [SpacetimeDB.Type] public partial struct NonEquatableViewPrimaryKey { + public uint Value; + public uint Extra; } [SpacetimeDB.Type] diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Type#NonEquatableViewPrimaryKey.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Type#NonEquatableViewPrimaryKey.verified.cs index b4d6dd416f9..7904014d2ad 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Type#NonEquatableViewPrimaryKey.verified.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Type#NonEquatableViewPrimaryKey.verified.cs @@ -9,11 +9,13 @@ partial struct NonEquatableViewPrimaryKey public void ReadFields(System.IO.BinaryReader reader) { Value = BSATN.ValueRW.Read(reader); + Extra = BSATN.ExtraRW.Read(reader); } public void WriteFields(System.IO.BinaryWriter writer) { BSATN.ValueRW.Write(writer, Value); + BSATN.ExtraRW.Write(writer, Extra); } object SpacetimeDB.BSATN.IStructuralReadWrite.GetSerializer() @@ -22,11 +24,12 @@ object SpacetimeDB.BSATN.IStructuralReadWrite.GetSerializer() } public override string ToString() => - $"NonEquatableViewPrimaryKey {{ Value = {SpacetimeDB.BSATN.StringUtil.GenericToString(Value)} }}"; + $"NonEquatableViewPrimaryKey {{ Value = {SpacetimeDB.BSATN.StringUtil.GenericToString(Value)}, Extra = {SpacetimeDB.BSATN.StringUtil.GenericToString(Extra)} }}"; public readonly partial struct BSATN : SpacetimeDB.BSATN.IReadWrite { internal static readonly SpacetimeDB.BSATN.U32 ValueRW = new(); + internal static readonly SpacetimeDB.BSATN.U32 ExtraRW = new(); public NonEquatableViewPrimaryKey Read(System.IO.BinaryReader reader) { @@ -47,7 +50,8 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar _ => new SpacetimeDB.BSATN.AlgebraicType.Product( new SpacetimeDB.BSATN.AggregateElement[] { - new("Value", ValueRW.GetAlgebraicType(registrar)) + new("Value", ValueRW.GetAlgebraicType(registrar)), + new("Extra", ExtraRW.GetAlgebraicType(registrar)) } ) ); @@ -60,14 +64,16 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar public override int GetHashCode() { var ___hashValue = Value.GetHashCode(); - return ___hashValue; + var ___hashExtra = Extra.GetHashCode(); + return ___hashValue ^ ___hashExtra; } #nullable enable public bool Equals(NonEquatableViewPrimaryKey that) { var ___eqValue = this.Value.Equals(that.Value); - return ___eqValue; + var ___eqExtra = this.Extra.Equals(that.Extra); + return ___eqValue && ___eqExtra; } public override bool Equals(object? that) diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/server/Lib.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/server/Lib.cs index 0a7ba3693eb..b4b8d840134 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/server/Lib.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/server/Lib.cs @@ -269,6 +269,25 @@ partial struct FormerlyForbiddenFieldNames public uint GetAlgebraicType; } +[SpacetimeDB.Type] +public partial struct ItemKey +{ + public ulong Value; + + public static implicit operator ulong(ItemKey key) => key.Value; + + public static implicit operator ItemKey(ulong value) => new() { Value = value }; +} + +[SpacetimeDB.Table(Name = "item", Public = true)] +public partial struct Item +{ + [SpacetimeDB.PrimaryKey] + public ItemKey Key; + + public string Name; +} + public class Module { [SpacetimeDB.ClientVisibilityFilter] diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs index da074b5e5f8..cd45871191b 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs @@ -108,6 +108,34 @@ public readonly partial struct QueryBuilder new("BTreeViews", new BTreeViewsCols("BTreeViews"), new BTreeViewsIxCols("BTreeViews")); } + public readonly struct ItemCols + { + public readonly global::SpacetimeDB.Col Key; + public readonly global::SpacetimeDB.Col Name; + + internal ItemCols(string tableName) + { + Key = new global::SpacetimeDB.Col(tableName, "Key"); + Name = new global::SpacetimeDB.Col(tableName, "Name"); + } + } + + public readonly struct ItemIxCols + { + public readonly global::SpacetimeDB.IxCol Key; + + internal ItemIxCols(string tableName) + { + Key = new global::SpacetimeDB.IxCol(tableName, "Key"); + } + } + + public readonly partial struct QueryBuilder + { + public global::SpacetimeDB.Table Item() => + new("Item", new ItemCols("Item"), new ItemIxCols("Item")); + } + public readonly struct MultiTable1Cols { public readonly global::SpacetimeDB.Col Name; @@ -713,6 +741,7 @@ public sealed class Local : global::SpacetimeDB.LocalBase internal global::SpacetimeDB.Internal.TableHandles.BTreeMultiColumn BTreeMultiColumn => new(); internal global::SpacetimeDB.Internal.TableHandles.BTreeViews BTreeViews => new(); + public global::SpacetimeDB.Internal.TableHandles.Item Item => new(); public global::SpacetimeDB.Internal.TableHandles.MultiTable1 MultiTable1 => new(); public global::SpacetimeDB.Internal.TableHandles.MultiTable2 MultiTable2 => new(); public global::SpacetimeDB.Internal.TableHandles.PrivateTable PrivateTable => new(); @@ -1128,6 +1157,77 @@ public ulong Delete(global::SpacetimeDB.Bound Faction) => internal FactionIndex Faction => new(); } + public readonly struct Item : global::SpacetimeDB.Internal.ITableView + { + public static global::Item ReadGenFields(System.IO.BinaryReader reader, global::Item row) + { + return row; + } + + public static SpacetimeDB.Internal.RawTableDefV10 MakeTableDesc( + SpacetimeDB.BSATN.ITypeRegistrar registrar + ) => + new( + SourceName: nameof(Item), + ProductTypeRef: (uint)new global::Item.BSATN().GetAlgebraicType(registrar).Ref_, + PrimaryKey: [0], + Indexes: + [ + new( + SourceName: "Item_Key_idx_btree", + AccessorName: "Key", + Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0]) + ) + ], + Constraints: + [ + global::SpacetimeDB.Internal.ITableView< + Item, + global::Item + >.MakeUniqueConstraint(0) + ], + Sequences: [], + TableType: SpacetimeDB.Internal.TableType.User, + TableAccess: SpacetimeDB.Internal.TableAccess.Public, + DefaultValues: [], + IsEvent: false + ); + + public static SpacetimeDB.Internal.RawScheduleDefV10? MakeScheduleDesc() => null; + + /// + /// Returns the number of rows in this table. + /// + /// This reads datastore metadata, so it runs in constant time. + /// It also takes into account modifications by the current transaction. + /// + public ulong Count => global::SpacetimeDB.Internal.ITableView.DoCount(); + + public IEnumerable Iter() => + global::SpacetimeDB.Internal.ITableView.DoIter(); + + public global::Item Insert(global::Item row) => + global::SpacetimeDB.Internal.ITableView.DoInsert(row); + + public bool Delete(global::Item row) => + global::SpacetimeDB.Internal.ITableView.DoDelete(row); + + public ulong Clear() => + global::SpacetimeDB.Internal.ITableView.DoClear(); + + public sealed class KeyUniqueIndex : UniqueIndex + { + internal KeyUniqueIndex() + : base("Item_Key_idx_btree") { } + + public global::Item? Find(ItemKey key) => FindSingle(key); + + public global::Item Update(global::Item row) => DoUpdate(row); + } + + public KeyUniqueIndex Key => new(); + } + public readonly struct MultiTable1 : global::SpacetimeDB.Internal.ITableView { @@ -2086,6 +2186,36 @@ internal FactionIndex() internal FactionIndex Faction => new(); } + public sealed class ItemReadOnly : global::SpacetimeDB.Internal.ReadOnlyTableView + { + internal ItemReadOnly() + : base("Item") { } + + /// + /// Returns the number of rows in this table. + /// + /// This reads datastore metadata, so it runs in constant time. + /// It also takes into account modifications by the current transaction. + /// + public ulong Count => DoCount(); + + public sealed class KeyIndex + : global::SpacetimeDB.Internal.ReadOnlyUniqueIndex< + global::SpacetimeDB.Internal.ViewHandles.ItemReadOnly, + global::Item, + ItemKey, + ItemKey.BSATN + > + { + internal KeyIndex() + : base("Item_Key_idx_btree") { } + + public global::Item? Find(ItemKey key) => FindSingle(key); + } + + public KeyIndex Key => new(); + } + public sealed class MultiTable1ReadOnly : global::SpacetimeDB.Internal.ReadOnlyTableView { @@ -2309,6 +2439,7 @@ public sealed partial class LocalReadOnly internal global::SpacetimeDB.Internal.ViewHandles.BTreeMultiColumnReadOnly BTreeMultiColumn => new(); internal global::SpacetimeDB.Internal.ViewHandles.BTreeViewsReadOnly BTreeViews => new(); + public global::SpacetimeDB.Internal.ViewHandles.ItemReadOnly Item => new(); public global::SpacetimeDB.Internal.ViewHandles.MultiTable1ReadOnly MultiTable1 => new(); public global::SpacetimeDB.Internal.ViewHandles.MultiTable2ReadOnly MultiTable2 => new(); public global::SpacetimeDB.Internal.ViewHandles.PrivateTableReadOnly PrivateTable => new(); @@ -2497,6 +2628,8 @@ public static void Main() (identity, connectionId, random, time) => new SpacetimeDB.ProcedureContext(identity, connectionId, random, time) ); + SpacetimeDB.Internal.Module.RegisterExplicitTableName("Item", "item"); + SpacetimeDB.Internal.Module.SetHandlerContextConstructor( (random, time) => new SpacetimeDB.HandlerContext(random, time) ); @@ -2525,6 +2658,10 @@ public static void Main() global::BTreeViews, SpacetimeDB.Internal.TableHandles.BTreeViews >(); + SpacetimeDB.Internal.Module.RegisterTable< + global::Item, + SpacetimeDB.Internal.TableHandles.Item + >(); SpacetimeDB.Internal.Module.RegisterTable< global::MultiTableRow, SpacetimeDB.Internal.TableHandles.MultiTable1 diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#Item.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#Item.verified.cs new file mode 100644 index 00000000000..1a613a4b72d --- /dev/null +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#Item.verified.cs @@ -0,0 +1,107 @@ +//HintName: Item.cs +// +#nullable enable + +partial struct Item : System.IEquatable, SpacetimeDB.BSATN.IStructuralReadWrite +{ + public void ReadFields(System.IO.BinaryReader reader) + { + Key = BSATN.KeyRW.Read(reader); + Name = BSATN.NameRW.Read(reader); + } + + public void WriteFields(System.IO.BinaryWriter writer) + { + BSATN.KeyRW.Write(writer, Key); + BSATN.NameRW.Write(writer, Name); + } + + object SpacetimeDB.BSATN.IStructuralReadWrite.GetSerializer() + { + return new BSATN(); + } + + public override string ToString() => + $"Item {{ Key = {SpacetimeDB.BSATN.StringUtil.GenericToString(Key)}, Name = {SpacetimeDB.BSATN.StringUtil.GenericToString(Name)} }}"; + + public readonly partial struct BSATN : SpacetimeDB.BSATN.IReadWrite + { + internal static readonly ItemKey.BSATN KeyRW = new(); + internal static readonly SpacetimeDB.BSATN.String NameRW = new(); + + public Item Read(System.IO.BinaryReader reader) + { + var ___result = new Item(); + ___result.ReadFields(reader); + return ___result; + } + + public void Write(System.IO.BinaryWriter writer, Item value) + { + value.WriteFields(writer); + } + + public SpacetimeDB.BSATN.AlgebraicType.Ref GetAlgebraicType( + SpacetimeDB.BSATN.ITypeRegistrar registrar + ) => + registrar.RegisterType(_ => new SpacetimeDB.BSATN.AlgebraicType.Product( + new SpacetimeDB.BSATN.AggregateElement[] + { + new("Key", KeyRW.GetAlgebraicType(registrar)), + new("Name", NameRW.GetAlgebraicType(registrar)) + } + )); + + SpacetimeDB.BSATN.AlgebraicType SpacetimeDB.BSATN.IReadWrite.GetAlgebraicType( + SpacetimeDB.BSATN.ITypeRegistrar registrar + ) => GetAlgebraicType(registrar); + } + + public override int GetHashCode() + { + var ___hashKey = Key.GetHashCode(); + var ___hashName = Name == null ? 0 : Name.GetHashCode(); + return ___hashKey ^ ___hashName; + } + +#nullable enable + public bool Equals(Item that) + { + var ___eqKey = this.Key.Equals(that.Key); + var ___eqName = this.Name == null ? that.Name == null : this.Name.Equals(that.Name); + return ___eqKey && ___eqName; + } + + public override bool Equals(object? that) + { + if (that == null) + { + return false; + } + var that_ = that as Item?; + if (((object?)that_) == null) + { + return false; + } + return Equals(that_); + } + + public static bool operator ==(Item this_, Item that) + { + if (((object?)this_) == null || ((object?)that) == null) + { + return object.Equals(this_, that); + } + return this_.Equals(that); + } + + public static bool operator !=(Item this_, Item that) + { + if (((object?)this_) == null || ((object?)that) == null) + { + return !object.Equals(this_, that); + } + return !this_.Equals(that); + } +#nullable restore +} // Item diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Type#ItemKey.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Type#ItemKey.verified.cs new file mode 100644 index 00000000000..c57175d5dcc --- /dev/null +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Type#ItemKey.verified.cs @@ -0,0 +1,101 @@ +//HintName: ItemKey.cs +// +#nullable enable + +partial struct ItemKey : System.IEquatable, SpacetimeDB.BSATN.IStructuralReadWrite +{ + public void ReadFields(System.IO.BinaryReader reader) + { + Value = BSATN.ValueRW.Read(reader); + } + + public void WriteFields(System.IO.BinaryWriter writer) + { + BSATN.ValueRW.Write(writer, Value); + } + + object SpacetimeDB.BSATN.IStructuralReadWrite.GetSerializer() + { + return new BSATN(); + } + + public override string ToString() => + $"ItemKey {{ Value = {SpacetimeDB.BSATN.StringUtil.GenericToString(Value)} }}"; + + public readonly partial struct BSATN : SpacetimeDB.BSATN.IReadWrite + { + internal static readonly SpacetimeDB.BSATN.U64 ValueRW = new(); + + public ItemKey Read(System.IO.BinaryReader reader) + { + var ___result = new ItemKey(); + ___result.ReadFields(reader); + return ___result; + } + + public void Write(System.IO.BinaryWriter writer, ItemKey value) + { + value.WriteFields(writer); + } + + public SpacetimeDB.BSATN.AlgebraicType.Ref GetAlgebraicType( + SpacetimeDB.BSATN.ITypeRegistrar registrar + ) => + registrar.RegisterType(_ => new SpacetimeDB.BSATN.AlgebraicType.Product( + new SpacetimeDB.BSATN.AggregateElement[] + { + new("Value", ValueRW.GetAlgebraicType(registrar)) + } + )); + + SpacetimeDB.BSATN.AlgebraicType SpacetimeDB.BSATN.IReadWrite.GetAlgebraicType( + SpacetimeDB.BSATN.ITypeRegistrar registrar + ) => GetAlgebraicType(registrar); + } + + public override int GetHashCode() + { + var ___hashValue = Value.GetHashCode(); + return ___hashValue; + } + +#nullable enable + public bool Equals(ItemKey that) + { + var ___eqValue = this.Value.Equals(that.Value); + return ___eqValue; + } + + public override bool Equals(object? that) + { + if (that == null) + { + return false; + } + var that_ = that as ItemKey?; + if (((object?)that_) == null) + { + return false; + } + return Equals(that_); + } + + public static bool operator ==(ItemKey this_, ItemKey that) + { + if (((object?)this_) == null || ((object?)that) == null) + { + return object.Equals(this_, that); + } + return this_.Equals(that); + } + + public static bool operator !=(ItemKey this_, ItemKey that) + { + if (((object?)this_) == null || ((object?)that) == null) + { + return !object.Equals(this_, that); + } + return !this_.Equals(that); + } +#nullable restore +} // ItemKey diff --git a/crates/bindings-csharp/Codegen/Module.cs b/crates/bindings-csharp/Codegen/Module.cs index f130a3efb05..8bd808aa720 100644 --- a/crates/bindings-csharp/Codegen/Module.cs +++ b/crates/bindings-csharp/Codegen/Module.cs @@ -174,8 +174,54 @@ public static bool IsEquatable(ITypeSymbol type) => or "SpacetimeDB.Uuid", _ => false, } + || IsNewtypeWrapper(type, out _) ) && type.NullableAnnotation != NullableAnnotation.Annotated; + + // Returns true when `type` is a [SpacetimeDB.Type] struct that wraps exactly one + // BSATN-annotated field whose own type passes IsEquatable. + public static bool IsNewtypeWrapper(ITypeSymbol type, out IFieldSymbol? underlyingField) + { + underlyingField = null; + + if (type.TypeKind != Microsoft.CodeAnalysis.TypeKind.Struct) + return false; + + if (type is not INamedTypeSymbol namedType) + return false; + + // Only structs annotated with [SpacetimeDB.Type] produce BSATN fields. + var typeAttrSyntaxes = namedType + .GetAttributes() + .Where(a => a.AttributeClass?.ToString() == "SpacetimeDB.TypeAttribute") + .Select(a => a.ApplicationSyntaxReference?.GetSyntax()) + .OfType() + .Select(a => a.FirstAncestorOrSelf()) + .OfType() + .Distinct() + .ToList(); + + if (typeAttrSyntaxes.Count == 0) + return false; + + var bsatnFields = typeAttrSyntaxes + .SelectMany(syntax => + SpacetimeDbFieldDiscovery.GetFieldsDeclaredInAnnotatedPartial(syntax, namedType)) + .ToList(); + + if (bsatnFields.Count != 1) + return false; + + underlyingField = bsatnFields[0]; + return IsEquatable(underlyingField.Type); + } + + public static string GetNewtypeEffectiveBsatnName(ITypeSymbol type) + { + if (IsNewtypeWrapper(type, out var inner) && inner != null) + return GetNewtypeEffectiveBsatnName(inner.Type); + return GetTypeInfo(type); + } } /// @@ -190,6 +236,9 @@ record ColumnDeclaration : MemberDeclaration public readonly string FullTableName; public readonly int ColumnIndex; public readonly string? ColumnDefaultValue; + // Non-null when the column type is a [SpacetimeDB.Type] newtype wrapper around a + // single equatable field. + public readonly string? NewtypeUnderlyingBsatnName; // A helper to combine multiple column attributes into a single mask. // Note: it doesn't check the table names, this is left up to the caller. @@ -246,6 +295,21 @@ public ColumnDeclaration(string tableName, int index, IFieldSymbol field, DiagRe diag.Report(ErrorDescriptor.UniqueNotEquatable, field); } + // When the column type is a newtype wrapper, capture the effective scalar's + if (ColumnTypeValidation.IsNewtypeWrapper(type, out _)) + { + try + { + NewtypeUnderlyingBsatnName = ColumnTypeValidation.GetNewtypeEffectiveBsatnName( + type + ); + } + catch + { + NewtypeUnderlyingBsatnName = null; + } + } + if ( attrs.HasFlag(ColumnAttrs.Default) && ( @@ -264,7 +328,10 @@ public ColumnAttrs GetAttrs(TableAccessor tableAccessor) => // For the `TableDesc` constructor. public string GenerateColumnDef() => - $"new (nameof({Identifier}), BSATN.{Identifier}{TypeUse.BsatnFieldSuffix}.GetAlgebraicType(registrar))"; + NewtypeUnderlyingBsatnName is { } underlyingBsatn + // Newtype wrapper: report the underlying scalar type so the server can build a B-tree PK index. + ? $"new (nameof({Identifier}), new {underlyingBsatn}().GetAlgebraicType(registrar))" + : $"new (nameof({Identifier}), BSATN.{Identifier}{TypeUse.BsatnFieldSuffix}.GetAlgebraicType(registrar))"; } record Scheduled(string ReducerName, int ScheduledAtColumn); @@ -297,7 +364,11 @@ attr.Scheduled is { } reducer Scheduled = new(reducer, scheduledAtIndex); if ( table.GetPrimaryKey(this) is not { } pk - || table.Members[pk].Type.Name != "ulong" + || ( + table.Members[pk].Type.Name != "ulong" + && table.Members[pk].NewtypeUnderlyingBsatnName + != "SpacetimeDB.BSATN.U64" + ) ) { throw new InvalidOperationException(