diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/Lib.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/Lib.cs index ef4e1ecf8c4..edbf55555fe 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/Lib.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/Lib.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using SpacetimeDB; public enum LocalEnum { } @@ -361,6 +362,80 @@ public static partial class InAnotherNamespace public partial struct TestDuplicateTableName { } } +[SpacetimeDB.Table] +public partial struct TestDefaultFieldValues +{ + [Unique] + public int? UniqueField; + + [Default("A default string set by attribute")] + public string DefaultString = ""; + + [Default(true)] + public bool DefaultBool = false; + + [Default((sbyte)2)] + public sbyte DefaultI8 = 1; + + [Default((byte)2)] + public byte DefaultU8 = 1; + + [Default((short)2)] + public short DefaultI16 = 1; + + [Default((ushort)2)] + public ushort DefaultU16 = 1; + + [Default(2)] + public int DefaultI32 = 1; + + [Default(2U)] + public uint DefaultU32 = 1U; + + [Default(2L)] + public long DefaultI64 = 1L; + + [Default(2UL)] + public ulong DefaultU64 = 1UL; + + [Default(0x02)] + public int DefaultHex = 1; + + [Default(0b00000010)] + public int DefaultBin = 1; + + [Default(2.0f)] + public float DefaultF32 = 1.0f; + + [Default(2.0)] + public double DefaultF64 = 1.0; + + [Default(MyEnum.SetByAttribute)] + public MyEnum DefaultEnum = MyEnum.SetByInitalization; + + [Default(null!)] + public MyStruct? DefaultNull = new MyStruct(1); +} + +[SpacetimeDB.Type] +public enum MyEnum +{ + Default, + SetByInitalization, + SetByAttribute, +} + +[SpacetimeDB.Type] +public partial struct MyStruct +{ + public int x; + + public MyStruct(int x) + { + this.x = x; + } +} + [SpacetimeDB.Table] [SpacetimeDB.Index.BTree(Name = "TestIndexWithoutColumns")] [SpacetimeDB.Index.BTree(Name = "TestIndexWithEmptyColumns", Columns = [])] diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/ExtraCompilationErrors.verified.txt b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/ExtraCompilationErrors.verified.txt index 158020f2fc6..a2d83c2492e 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/ExtraCompilationErrors.verified.txt +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/ExtraCompilationErrors.verified.txt @@ -92,10 +92,33 @@ var ___hashUnsupportedEnum = UnsupportedEnum.GetHashCode(); } }, {/* +[SpacetimeDB.Table] +public partial struct TestDefaultFieldValues + ^^^^^^^^^^^^^^^^^^^^^^ +{ +*/ + Message: A 'struct' with field initializers must include an explicitly declared constructor., + Severity: Error, + Descriptor: { + Id: CS8983, + Title: , + HelpLink: https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS8983), + MessageFormat: A 'struct' with field initializers must include an explicitly declared constructor., + Category: Compiler, + DefaultSeverity: Error, + IsEnabledByDefault: true, + CustomTags: [ + Compiler, + Telemetry, + NotConfigurable + ] + } + }, + {/* SpacetimeDB.Internal.Module.RegisterClientVisibilityFilter(global::Module.MY_SECOND_FILTER); SpacetimeDB.Internal.Module.RegisterClientVisibilityFilter(global::Module.MY_THIRD_FILTER); ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - } + { */ Message: Argument 1: cannot convert from 'string' to 'SpacetimeDB.Filter', Severity: Error, 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 9b9688f86fb..6ecaba916e3 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 @@ -145,6 +145,75 @@ internal IdentityFieldUniqueIndex() public IdentityFieldUniqueIndex IdentityField => new(); } + public readonly struct TestDefaultFieldValues + : SpacetimeDB.Internal.ITableView< + TestDefaultFieldValues, + global::TestDefaultFieldValues + > + { + static global::TestDefaultFieldValues SpacetimeDB.Internal.ITableView< + TestDefaultFieldValues, + global::TestDefaultFieldValues + >.ReadGenFields(System.IO.BinaryReader reader, global::TestDefaultFieldValues row) + { + return row; + } + + static SpacetimeDB.Internal.RawTableDefV9 SpacetimeDB.Internal.ITableView< + TestDefaultFieldValues, + global::TestDefaultFieldValues + >.MakeTableDesc(SpacetimeDB.BSATN.ITypeRegistrar registrar) => + new( + Name: nameof(TestDefaultFieldValues), + ProductTypeRef: (uint) + new global::TestDefaultFieldValues.BSATN().GetAlgebraicType(registrar).Ref_, + PrimaryKey: [], + Indexes: + [ + new( + Name: null, + AccessorName: "UniqueField", + Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0]) + ) + ], + Constraints: + [ + SpacetimeDB.Internal.ITableView< + TestDefaultFieldValues, + global::TestDefaultFieldValues + >.MakeUniqueConstraint(0) + ], + Sequences: [], + Schedule: null, + TableType: SpacetimeDB.Internal.TableType.User, + TableAccess: SpacetimeDB.Internal.TableAccess.Private + ); + + public ulong Count => + SpacetimeDB.Internal.ITableView< + TestDefaultFieldValues, + global::TestDefaultFieldValues + >.DoCount(); + + public IEnumerable Iter() => + SpacetimeDB.Internal.ITableView< + TestDefaultFieldValues, + global::TestDefaultFieldValues + >.DoIter(); + + public global::TestDefaultFieldValues Insert(global::TestDefaultFieldValues row) => + SpacetimeDB.Internal.ITableView< + TestDefaultFieldValues, + global::TestDefaultFieldValues + >.DoInsert(row); + + public bool Delete(global::TestDefaultFieldValues row) => + SpacetimeDB.Internal.ITableView< + TestDefaultFieldValues, + global::TestDefaultFieldValues + >.DoDelete(row); + } + public readonly struct TestDuplicateTableName : SpacetimeDB.Internal.ITableView< TestDuplicateTableName, @@ -827,6 +896,7 @@ internal PrimaryKeyFieldUniqueIndex() public sealed class Local { public Internal.TableHandles.TestAutoIncNotInteger TestAutoIncNotInteger => new(); + public Internal.TableHandles.TestDefaultFieldValues TestDefaultFieldValues => new(); public Internal.TableHandles.TestDuplicateTableName TestDuplicateTableName => new(); public Internal.TableHandles.TestIndexIssues TestIndexIssues => new(); public Internal.TableHandles.TestScheduleWithMissingScheduleAtField TestScheduleWithMissingScheduleAtField => @@ -968,6 +1038,8 @@ public static void Main() (identity, connectionId, random, time) => new SpacetimeDB.ReducerContext(identity, connectionId, random, time) ); + var __memoryStream = new MemoryStream(); + var __writer = new BinaryWriter(__memoryStream); SpacetimeDB.Internal.Module.RegisterReducer<__ReducerWithReservedPrefix>(); SpacetimeDB.Internal.Module.RegisterReducer(); @@ -981,6 +1053,10 @@ public static void Main() global::TestAutoIncNotInteger, SpacetimeDB.Internal.TableHandles.TestAutoIncNotInteger >(); + SpacetimeDB.Internal.Module.RegisterTable< + global::TestDefaultFieldValues, + SpacetimeDB.Internal.TableHandles.TestDefaultFieldValues + >(); SpacetimeDB.Internal.Module.RegisterTable< global::TestDuplicateTableName, SpacetimeDB.Internal.TableHandles.TestDuplicateTableName @@ -1017,6 +1093,213 @@ public static void Main() SpacetimeDB.Internal.Module.RegisterClientVisibilityFilter(global::Module.MY_FOURTH_FILTER); SpacetimeDB.Internal.Module.RegisterClientVisibilityFilter(global::Module.MY_SECOND_FILTER); SpacetimeDB.Internal.Module.RegisterClientVisibilityFilter(global::Module.MY_THIRD_FILTER); + { + var value = new SpacetimeDB.BSATN.String(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, "A default string set by attribute"); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 1, + array + ); + } + + { + var value = new SpacetimeDB.BSATN.U64(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, 2); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 10, + array + ); + } + + { + var value = new SpacetimeDB.BSATN.I32(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, 2); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 11, + array + ); + } + + { + var value = new SpacetimeDB.BSATN.I32(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, 2); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 12, + array + ); + } + + { + var value = new SpacetimeDB.BSATN.F32(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, 2); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 13, + array + ); + } + + { + var value = new SpacetimeDB.BSATN.F64(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, 2); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 14, + array + ); + } + + { + var value = new SpacetimeDB.BSATN.Enum(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, (MyEnum)2); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 15, + array + ); + } + + { + var value = new SpacetimeDB.BSATN.ValueOption(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, null); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 16, + array + ); + } + + { + var value = new SpacetimeDB.BSATN.Bool(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, true); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 2, + array + ); + } + + { + var value = new SpacetimeDB.BSATN.I8(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, 2); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 3, + array + ); + } + + { + var value = new SpacetimeDB.BSATN.U8(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, 2); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 4, + array + ); + } + + { + var value = new SpacetimeDB.BSATN.I16(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, 2); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 5, + array + ); + } + + { + var value = new SpacetimeDB.BSATN.U16(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, 2); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 6, + array + ); + } + + { + var value = new SpacetimeDB.BSATN.I32(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, 2); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 7, + array + ); + } + + { + var value = new SpacetimeDB.BSATN.U32(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, 2); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 8, + array + ); + } + + { + var value = new SpacetimeDB.BSATN.I64(); + __memoryStream.Position = 0; + __memoryStream.SetLength(0); + value.Write(__writer, 2); + var array = __memoryStream.ToArray(); + SpacetimeDB.Internal.Module.RegisterTableDefaultValue( + "TestDefaultFieldValues", + 9, + array + ); + } } // Exports only work from the main assembly, so we need to generate forwarding methods. diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#TestDefaultFieldValues.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#TestDefaultFieldValues.verified.cs new file mode 100644 index 00000000000..23417c51a5e --- /dev/null +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#TestDefaultFieldValues.verified.cs @@ -0,0 +1,242 @@ +//HintName: TestDefaultFieldValues.cs +// +#nullable enable + +partial struct TestDefaultFieldValues + : System.IEquatable, + SpacetimeDB.BSATN.IStructuralReadWrite +{ + public void ReadFields(System.IO.BinaryReader reader) + { + UniqueField = BSATN.UniqueFieldRW.Read(reader); + DefaultString = BSATN.DefaultStringRW.Read(reader); + DefaultBool = BSATN.DefaultBoolRW.Read(reader); + DefaultI8 = BSATN.DefaultI8RW.Read(reader); + DefaultU8 = BSATN.DefaultU8RW.Read(reader); + DefaultI16 = BSATN.DefaultI16RW.Read(reader); + DefaultU16 = BSATN.DefaultU16RW.Read(reader); + DefaultI32 = BSATN.DefaultI32RW.Read(reader); + DefaultU32 = BSATN.DefaultU32RW.Read(reader); + DefaultI64 = BSATN.DefaultI64RW.Read(reader); + DefaultU64 = BSATN.DefaultU64RW.Read(reader); + DefaultHex = BSATN.DefaultHexRW.Read(reader); + DefaultBin = BSATN.DefaultBinRW.Read(reader); + DefaultF32 = BSATN.DefaultF32RW.Read(reader); + DefaultF64 = BSATN.DefaultF64RW.Read(reader); + DefaultEnum = BSATN.DefaultEnumRW.Read(reader); + DefaultNull = BSATN.DefaultNullRW.Read(reader); + } + + public void WriteFields(System.IO.BinaryWriter writer) + { + BSATN.UniqueFieldRW.Write(writer, UniqueField); + BSATN.DefaultStringRW.Write(writer, DefaultString); + BSATN.DefaultBoolRW.Write(writer, DefaultBool); + BSATN.DefaultI8RW.Write(writer, DefaultI8); + BSATN.DefaultU8RW.Write(writer, DefaultU8); + BSATN.DefaultI16RW.Write(writer, DefaultI16); + BSATN.DefaultU16RW.Write(writer, DefaultU16); + BSATN.DefaultI32RW.Write(writer, DefaultI32); + BSATN.DefaultU32RW.Write(writer, DefaultU32); + BSATN.DefaultI64RW.Write(writer, DefaultI64); + BSATN.DefaultU64RW.Write(writer, DefaultU64); + BSATN.DefaultHexRW.Write(writer, DefaultHex); + BSATN.DefaultBinRW.Write(writer, DefaultBin); + BSATN.DefaultF32RW.Write(writer, DefaultF32); + BSATN.DefaultF64RW.Write(writer, DefaultF64); + BSATN.DefaultEnumRW.Write(writer, DefaultEnum); + BSATN.DefaultNullRW.Write(writer, DefaultNull); + } + + object SpacetimeDB.BSATN.IStructuralReadWrite.GetSerializer() + { + return new BSATN(); + } + + public override string ToString() => + $"TestDefaultFieldValues {{ UniqueField = {SpacetimeDB.BSATN.StringUtil.GenericToString(UniqueField)}, DefaultString = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultString)}, DefaultBool = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultBool)}, DefaultI8 = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultI8)}, DefaultU8 = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultU8)}, DefaultI16 = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultI16)}, DefaultU16 = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultU16)}, DefaultI32 = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultI32)}, DefaultU32 = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultU32)}, DefaultI64 = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultI64)}, DefaultU64 = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultU64)}, DefaultHex = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultHex)}, DefaultBin = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultBin)}, DefaultF32 = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultF32)}, DefaultF64 = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultF64)}, DefaultEnum = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultEnum)}, DefaultNull = {SpacetimeDB.BSATN.StringUtil.GenericToString(DefaultNull)} }}"; + + public readonly partial struct BSATN : SpacetimeDB.BSATN.IReadWrite + { + internal static readonly SpacetimeDB.BSATN.ValueOption< + int, + SpacetimeDB.BSATN.I32 + > UniqueFieldRW = new(); + internal static readonly SpacetimeDB.BSATN.String DefaultStringRW = new(); + internal static readonly SpacetimeDB.BSATN.Bool DefaultBoolRW = new(); + internal static readonly SpacetimeDB.BSATN.I8 DefaultI8RW = new(); + internal static readonly SpacetimeDB.BSATN.U8 DefaultU8RW = new(); + internal static readonly SpacetimeDB.BSATN.I16 DefaultI16RW = new(); + internal static readonly SpacetimeDB.BSATN.U16 DefaultU16RW = new(); + internal static readonly SpacetimeDB.BSATN.I32 DefaultI32RW = new(); + internal static readonly SpacetimeDB.BSATN.U32 DefaultU32RW = new(); + internal static readonly SpacetimeDB.BSATN.I64 DefaultI64RW = new(); + internal static readonly SpacetimeDB.BSATN.U64 DefaultU64RW = new(); + internal static readonly SpacetimeDB.BSATN.I32 DefaultHexRW = new(); + internal static readonly SpacetimeDB.BSATN.I32 DefaultBinRW = new(); + internal static readonly SpacetimeDB.BSATN.F32 DefaultF32RW = new(); + internal static readonly SpacetimeDB.BSATN.F64 DefaultF64RW = new(); + internal static readonly SpacetimeDB.BSATN.Enum DefaultEnumRW = new(); + internal static readonly SpacetimeDB.BSATN.ValueOption< + MyStruct, + MyStruct.BSATN + > DefaultNullRW = new(); + + public TestDefaultFieldValues Read(System.IO.BinaryReader reader) + { + var ___result = new TestDefaultFieldValues(); + ___result.ReadFields(reader); + return ___result; + } + + public void Write(System.IO.BinaryWriter writer, TestDefaultFieldValues 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("UniqueField", UniqueFieldRW.GetAlgebraicType(registrar)), + new("DefaultString", DefaultStringRW.GetAlgebraicType(registrar)), + new("DefaultBool", DefaultBoolRW.GetAlgebraicType(registrar)), + new("DefaultI8", DefaultI8RW.GetAlgebraicType(registrar)), + new("DefaultU8", DefaultU8RW.GetAlgebraicType(registrar)), + new("DefaultI16", DefaultI16RW.GetAlgebraicType(registrar)), + new("DefaultU16", DefaultU16RW.GetAlgebraicType(registrar)), + new("DefaultI32", DefaultI32RW.GetAlgebraicType(registrar)), + new("DefaultU32", DefaultU32RW.GetAlgebraicType(registrar)), + new("DefaultI64", DefaultI64RW.GetAlgebraicType(registrar)), + new("DefaultU64", DefaultU64RW.GetAlgebraicType(registrar)), + new("DefaultHex", DefaultHexRW.GetAlgebraicType(registrar)), + new("DefaultBin", DefaultBinRW.GetAlgebraicType(registrar)), + new("DefaultF32", DefaultF32RW.GetAlgebraicType(registrar)), + new("DefaultF64", DefaultF64RW.GetAlgebraicType(registrar)), + new("DefaultEnum", DefaultEnumRW.GetAlgebraicType(registrar)), + new("DefaultNull", DefaultNullRW.GetAlgebraicType(registrar)) + } + ) + ); + + SpacetimeDB.BSATN.AlgebraicType SpacetimeDB.BSATN.IReadWrite.GetAlgebraicType( + SpacetimeDB.BSATN.ITypeRegistrar registrar + ) => GetAlgebraicType(registrar); + } + + public override int GetHashCode() + { + var ___hashUniqueField = UniqueField.GetHashCode(); + var ___hashDefaultString = DefaultString == null ? 0 : DefaultString.GetHashCode(); + var ___hashDefaultBool = DefaultBool.GetHashCode(); + var ___hashDefaultI8 = DefaultI8.GetHashCode(); + var ___hashDefaultU8 = DefaultU8.GetHashCode(); + var ___hashDefaultI16 = DefaultI16.GetHashCode(); + var ___hashDefaultU16 = DefaultU16.GetHashCode(); + var ___hashDefaultI32 = DefaultI32.GetHashCode(); + var ___hashDefaultU32 = DefaultU32.GetHashCode(); + var ___hashDefaultI64 = DefaultI64.GetHashCode(); + var ___hashDefaultU64 = DefaultU64.GetHashCode(); + var ___hashDefaultHex = DefaultHex.GetHashCode(); + var ___hashDefaultBin = DefaultBin.GetHashCode(); + var ___hashDefaultF32 = DefaultF32.GetHashCode(); + var ___hashDefaultF64 = DefaultF64.GetHashCode(); + var ___hashDefaultEnum = DefaultEnum.GetHashCode(); + var ___hashDefaultNull = DefaultNull.GetHashCode(); + return ___hashUniqueField + ^ ___hashDefaultString + ^ ___hashDefaultBool + ^ ___hashDefaultI8 + ^ ___hashDefaultU8 + ^ ___hashDefaultI16 + ^ ___hashDefaultU16 + ^ ___hashDefaultI32 + ^ ___hashDefaultU32 + ^ ___hashDefaultI64 + ^ ___hashDefaultU64 + ^ ___hashDefaultHex + ^ ___hashDefaultBin + ^ ___hashDefaultF32 + ^ ___hashDefaultF64 + ^ ___hashDefaultEnum + ^ ___hashDefaultNull; + } + +#nullable enable + public bool Equals(TestDefaultFieldValues that) + { + var ___eqUniqueField = this.UniqueField.Equals(that.UniqueField); + var ___eqDefaultString = + this.DefaultString == null + ? that.DefaultString == null + : this.DefaultString.Equals(that.DefaultString); + var ___eqDefaultBool = this.DefaultBool.Equals(that.DefaultBool); + var ___eqDefaultI8 = this.DefaultI8.Equals(that.DefaultI8); + var ___eqDefaultU8 = this.DefaultU8.Equals(that.DefaultU8); + var ___eqDefaultI16 = this.DefaultI16.Equals(that.DefaultI16); + var ___eqDefaultU16 = this.DefaultU16.Equals(that.DefaultU16); + var ___eqDefaultI32 = this.DefaultI32.Equals(that.DefaultI32); + var ___eqDefaultU32 = this.DefaultU32.Equals(that.DefaultU32); + var ___eqDefaultI64 = this.DefaultI64.Equals(that.DefaultI64); + var ___eqDefaultU64 = this.DefaultU64.Equals(that.DefaultU64); + var ___eqDefaultHex = this.DefaultHex.Equals(that.DefaultHex); + var ___eqDefaultBin = this.DefaultBin.Equals(that.DefaultBin); + var ___eqDefaultF32 = this.DefaultF32.Equals(that.DefaultF32); + var ___eqDefaultF64 = this.DefaultF64.Equals(that.DefaultF64); + var ___eqDefaultEnum = this.DefaultEnum == that.DefaultEnum; + var ___eqDefaultNull = this.DefaultNull.Equals(that.DefaultNull); + return ___eqUniqueField + && ___eqDefaultString + && ___eqDefaultBool + && ___eqDefaultI8 + && ___eqDefaultU8 + && ___eqDefaultI16 + && ___eqDefaultU16 + && ___eqDefaultI32 + && ___eqDefaultU32 + && ___eqDefaultI64 + && ___eqDefaultU64 + && ___eqDefaultHex + && ___eqDefaultBin + && ___eqDefaultF32 + && ___eqDefaultF64 + && ___eqDefaultEnum + && ___eqDefaultNull; + } + + public override bool Equals(object? that) + { + if (that == null) + { + return false; + } + var that_ = that as TestDefaultFieldValues?; + if (((object?)that_) == null) + { + return false; + } + return Equals(that_); + } + + public static bool operator ==(TestDefaultFieldValues this_, TestDefaultFieldValues that) + { + if (((object?)this_) == null || ((object?)that) == null) + { + return object.Equals(this_, that); + } + return this_.Equals(that); + } + + public static bool operator !=(TestDefaultFieldValues this_, TestDefaultFieldValues that) + { + if (((object?)this_) == null || ((object?)that) == null) + { + return !object.Equals(this_, that); + } + return !this_.Equals(that); + } +#nullable restore +} // TestDefaultFieldValues 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 66f3671d130..422ef7c44c9 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 @@ -69,6 +69,23 @@ public partial record TestTableTaggedEnum : SpacetimeDB.TaggedEnum<(int X, int Y } }, {/* + [Unique] + public int? UniqueField; + ^^^^^^^^^^^ + +*/ + Message: Field UniqueField is marked as Unique but it has a type int? 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.Index.BTree(Name = "TestUnexpectedColumns", Columns = ["UnexpectedColumn"])] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -412,4 +429,4 @@ public partial struct TestScheduleIssues } } ] -} +} \ No newline at end of file diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Type#MyStruct.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Type#MyStruct.verified.cs new file mode 100644 index 00000000000..ab145aab509 --- /dev/null +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Type#MyStruct.verified.cs @@ -0,0 +1,101 @@ +//HintName: MyStruct.cs +// +#nullable enable + +partial struct MyStruct : System.IEquatable, SpacetimeDB.BSATN.IStructuralReadWrite +{ + public void ReadFields(System.IO.BinaryReader reader) + { + x = BSATN.xRW.Read(reader); + } + + public void WriteFields(System.IO.BinaryWriter writer) + { + BSATN.xRW.Write(writer, x); + } + + object SpacetimeDB.BSATN.IStructuralReadWrite.GetSerializer() + { + return new BSATN(); + } + + public override string ToString() => + $"MyStruct {{ x = {SpacetimeDB.BSATN.StringUtil.GenericToString(x)} }}"; + + public readonly partial struct BSATN : SpacetimeDB.BSATN.IReadWrite + { + internal static readonly SpacetimeDB.BSATN.I32 xRW = new(); + + public MyStruct Read(System.IO.BinaryReader reader) + { + var ___result = new MyStruct(); + ___result.ReadFields(reader); + return ___result; + } + + public void Write(System.IO.BinaryWriter writer, MyStruct 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("x", xRW.GetAlgebraicType(registrar)) + } + )); + + SpacetimeDB.BSATN.AlgebraicType SpacetimeDB.BSATN.IReadWrite.GetAlgebraicType( + SpacetimeDB.BSATN.ITypeRegistrar registrar + ) => GetAlgebraicType(registrar); + } + + public override int GetHashCode() + { + var ___hashx = x.GetHashCode(); + return ___hashx; + } + +#nullable enable + public bool Equals(MyStruct that) + { + var ___eqx = this.x.Equals(that.x); + return ___eqx; + } + + public override bool Equals(object? that) + { + if (that == null) + { + return false; + } + var that_ = that as MyStruct?; + if (((object?)that_) == null) + { + return false; + } + return Equals(that_); + } + + public static bool operator ==(MyStruct this_, MyStruct that) + { + if (((object?)this_) == null || ((object?)that) == null) + { + return object.Equals(this_, that); + } + return this_.Equals(that); + } + + public static bool operator !=(MyStruct this_, MyStruct that) + { + if (((object?)this_) == null || ((object?)that) == null) + { + return !object.Equals(this_, that); + } + return !this_.Equals(that); + } +#nullable restore +} // MyStruct 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 88c501e4e6e..682405b91c0 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 @@ -1092,6 +1092,8 @@ public static void Main() (identity, connectionId, random, time) => new SpacetimeDB.ReducerContext(identity, connectionId, random, time) ); + var __memoryStream = new MemoryStream(); + var __writer = new BinaryWriter(__memoryStream); SpacetimeDB.Internal.Module.RegisterReducer(); SpacetimeDB.Internal.Module.RegisterReducer(); diff --git a/crates/bindings-csharp/Codegen/Diag.cs b/crates/bindings-csharp/Codegen/Diag.cs index caad0abd101..83d3f8afa3f 100644 --- a/crates/bindings-csharp/Codegen/Diag.cs +++ b/crates/bindings-csharp/Codegen/Diag.cs @@ -150,4 +150,29 @@ string typeName $"Field {field.Name} is marked as [ClientVisibilityFilter] but it is not public static readonly", field => field ); + + public static readonly ErrorDescriptor IncompatibleDefaultAttributesCombination = + new( + group, + "Invalid Combination: AutoInc, Unique or PrimaryKey cannot have a Default value", + field => + $"Field {field.Name} contains a default value and has a AutoInc, Unique or PrimaryKey attributes, which is not allowed.", + field => field + ); + + public static readonly ErrorDescriptor InvalidDefaultValueType = + new( + group, + "Invalid Default Value Type", + field => $"Default value for field {field.Name} cannot be converted to provided type", + field => field + ); + + public static readonly ErrorDescriptor InvalidDefaultValueFormat = + new( + group, + "Invalid Default Value Format", + field => $"Default value for field {field.Name} has invalid format for provided type ", + field => field + ); } diff --git a/crates/bindings-csharp/Codegen/Module.cs b/crates/bindings-csharp/Codegen/Module.cs index fb9f1d6fd79..a21e25053fa 100644 --- a/crates/bindings-csharp/Codegen/Module.cs +++ b/crates/bindings-csharp/Codegen/Module.cs @@ -8,12 +8,30 @@ namespace SpacetimeDB.Codegen; using SpacetimeDB.Internal; using static Utils; -readonly record struct ColumnAttr(ColumnAttrs Mask, string? Table = null) +/// +/// Represents column attributes parsed from field attributes in table classes. +/// Used to track metadata like primary keys, unique constraints, and default values. +/// +/// Bitmask representing the column attributes (PrimaryKey, Unique, etc.) +/// Optional table name if the attribute is table-specific +/// Optional value for attributes like Default that carry additional data +readonly record struct ColumnAttr(ColumnAttrs Mask, string? Table = null, string? Value = null) { + // Maps attribute type names to their corresponding attribute types private static readonly ImmutableDictionary AttrTypes = ImmutableArray - .Create(typeof(AutoIncAttribute), typeof(PrimaryKeyAttribute), typeof(UniqueAttribute)) - .ToImmutableDictionary(t => t.FullName); + .Create( + typeof(AutoIncAttribute), + typeof(PrimaryKeyAttribute), + typeof(UniqueAttribute), + typeof(DefaultAttribute) + ) + .ToImmutableDictionary(t => t.FullName!); + /// + /// Parses a Roslyn AttributeData into a ColumnAttr instance. + /// + /// The attribute data to parse + /// A ColumnAttr instance representing the parsed attribute, or default if the attribute type is not recognized public static ColumnAttr Parse(AttributeData attrData) { if ( @@ -23,19 +41,40 @@ attrData.AttributeClass is not { } attrClass { return default; } + + // Special handling for DefaultAttribute as it contains an additional value + if (attrClass.ToString() == typeof(DefaultAttribute).FullName) + { + var defaultAttr = attrData.ParseAs(attrType); + return new(defaultAttr.Mask, defaultAttr.Table, defaultAttr.Value); + } + + // Handle standard column attributes (PrimaryKey, Unique, AutoInc) var attr = attrData.ParseAs(attrType); return new(attr.Mask, attr.Table); } } +/// +/// Represents a reference to a column in a table, combining its index and name. +/// Used to maintain references to columns for indexing and querying purposes. +/// +/// The zero-based index of the column in the table +/// The name of the column as defined in the source code record ColumnRef(int Index, string Name); +/// +/// Represents the declaration of a column in a table. +/// Contains metadata and attributes for the column, including its type, constraints, and indexes. +/// record ColumnDeclaration : MemberDeclaration { public readonly EquatableArray Attrs; public readonly EquatableArray Indexes; public readonly bool IsEquatable; public readonly string FullTableName; + public readonly int ColumnIndex; + public readonly string? ColumnDefaultValue; // 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. @@ -46,6 +85,7 @@ public ColumnDeclaration(string tableName, int index, IFieldSymbol field, DiagRe : base(field, diag) { FullTableName = tableName; + ColumnIndex = index; Attrs = new( field @@ -67,6 +107,14 @@ public ColumnDeclaration(string tableName, int index, IFieldSymbol field, DiagRe .ToImmutableArray() ); + ColumnDefaultValue = field + .GetAttributes() + .Select(ColumnAttr.Parse) + .Where(a => a.Mask == ColumnAttrs.Default) + .Select(a => a.Value) + .ToList() + .FirstOrDefault(); + var type = field.Type; var isInteger = type.SpecialType switch @@ -137,6 +185,18 @@ or SpecialType.System_Int64 { diag.Report(ErrorDescriptor.UniqueNotEquatable, field); } + + if ( + attrs.HasFlag(ColumnAttrs.Default) + && ( + attrs.HasFlag(ColumnAttrs.AutoInc) + || attrs.HasFlag(ColumnAttrs.PrimaryKey) + || attrs.HasFlag(ColumnAttrs.Unique) + ) + ) + { + diag.Report(ErrorDescriptor.IncompatibleDefaultAttributesCombination, field); + } } public ColumnAttrs GetAttrs(TableView view) => @@ -200,6 +260,10 @@ enum ViewIndexType BTree, } +/// +/// Represents an index on a database table view, used to optimize queries. +/// Supports B-tree indexing (and potentially other types in the future). +/// record ViewIndex { public readonly EquatableArray Columns; @@ -211,6 +275,14 @@ record ViewIndex // Guaranteed not to contain quotes, so does not need to be escaped when embedded in a string. private readonly string StandardNameSuffix; + /// + /// Primary constructor that initializes all fields. + /// Other constructors delegate to this one to avoid code duplication. + /// + /// Name to use when accessing this index. If null, will be generated from column names. + /// The columns that make up this index. + /// The name of the table this index belongs to, if any. + /// The type of index (currently only B-tree is supported). private ViewIndex( string? accessorName, ImmutableArray columns, @@ -226,6 +298,10 @@ ViewIndexType type StandardNameSuffix = $"_{columnNames}_idx_{Type.ToString().ToLower()}"; } + /// + /// Creates a B-tree index on a single column with auto-generated name. + /// + /// The column to index. public ViewIndex(ColumnRef col) : this( null, @@ -234,9 +310,17 @@ public ViewIndex(ColumnRef col) ViewIndexType.BTree // this might become hash in the future ) { } + /// + /// Creates an index with the given attribute and columns. + /// Used internally by other constructors that parse attributes. + /// private ViewIndex(Index.BTreeAttribute attr, ImmutableArray columns) : this(attr.Name, columns, attr.Table, ViewIndexType.BTree) { } + /// + /// Creates an index from a table declaration and attribute data. + /// Validates the index configuration and reports any errors through the diag reporter. + /// private ViewIndex( TableDeclaration table, Index.BTreeAttribute attr, @@ -259,9 +343,16 @@ DiagReporter diag } } + /// + /// Creates an index by parsing attribute data from a table declaration. + /// public ViewIndex(TableDeclaration table, AttributeData data, DiagReporter diag) : this(table, data.ParseAs(), data, diag) { } + /// + /// Creates an index for a single column with attribute data. + /// Validates that no additional columns were specified in the attribute. + /// private ViewIndex( ColumnRef column, Index.BTreeAttribute attr, @@ -276,6 +367,9 @@ DiagReporter diag } } + /// + /// Creates an index for a single column by parsing attribute data. + /// public ViewIndex(ColumnRef col, AttributeData data, DiagReporter diag) : this(col, data.ParseAs(), data, diag) { } @@ -305,6 +399,10 @@ public string GenerateIndexDef() => public string StandardIndexName(TableView view) => view.Name + StandardNameSuffix; } +/// +/// Represents a table declaration in a module. +/// Handles table metadata, views, indexes, and column declarations for code generation. +/// record TableDeclaration : BaseTypeDeclaration { public readonly Accessibility Visibility; @@ -466,8 +564,21 @@ public ulong Delete({{argsBounds}}) => } } + /// + /// Represents a generated view for a table, providing different access patterns + /// and visibility levels for the underlying table data. + /// + /// Name of the generated view type + /// Fully qualified name of the table type + /// C# source code for the view implementation + /// C# property getter for accessing the view public record struct View(string viewName, string tableName, string view, string getter); + /// + /// Generates view implementations for all table views defined in this table declaration. + /// Each view represents a different way to access or filter the table's data. + /// + /// Collection of View records containing generated code for each view public IEnumerable GenerateViews() { // Don't try to generate views if this table is a sum type. @@ -539,6 +650,78 @@ v.Scheduled is { } scheduled } } + /// + /// Represents a default value for a table field, used during table creation. + /// + /// Name of the table containing the field + /// Index of the column in the table + /// String representation of the default value + /// BSATN Type name of the default value + public record struct FieldDefaultValue( + string tableName, + string columnId, + string value, + string BSATNTypeName + ); + + /// + /// Generates default values for table fields with the [Default] attribute. + /// These values are used when creating new rows without explicit values for the corresponding fields. + /// + /// Collection of default values for fields that specify them + public IEnumerable GenerateDefaultValues() + { + if (Kind is TypeKind.Sum) + { + yield break; + } + + foreach (var view in Views) + { + var members = string.Join(", ", Members.Select(m => m.Name)); + var fieldsWithDefaultValues = Members.Where(m => + m.GetAttrs(view).HasFlag(ColumnAttrs.Default) + ); + var defaultValueAttributes = string.Join( + ", ", + Members + .Where(m => m.GetAttrs(view).HasFlag(ColumnAttrs.Default)) + .Select(m => m.Attrs.FirstOrDefault(a => a.Mask == ColumnAttrs.Default)) + ); + + var withDefaultValues = + fieldsWithDefaultValues as ColumnDeclaration[] ?? fieldsWithDefaultValues.ToArray(); + foreach (var fieldsWithDefaultValue in withDefaultValues) + { + if ( + fieldsWithDefaultValue.ColumnDefaultValue != null + && fieldsWithDefaultValue.Type.BSATNName != "" + ) + { + // For enums, we'll need to wrap the default value in the enum type. + if (fieldsWithDefaultValue.Type.BSATNName.StartsWith("SpacetimeDB.BSATN.Enum")) + { + yield return new FieldDefaultValue( + view.Name, + fieldsWithDefaultValue.ColumnIndex.ToString(), + $"({fieldsWithDefaultValue.Type.Name}){fieldsWithDefaultValue.ColumnDefaultValue}", + fieldsWithDefaultValue.Type.BSATNName + ); + } + else + { + yield return new FieldDefaultValue( + view.Name, + fieldsWithDefaultValue.ColumnIndex.ToString(), + fieldsWithDefaultValue.ColumnDefaultValue, + fieldsWithDefaultValue.Type.BSATNName + ); + } + } + } + } + } + public record Constraint(ColumnDeclaration Col, int Pos, ColumnAttrs Attr) { public ViewIndex ToIndex() => new(new ColumnRef(Pos, Col.Name)); @@ -580,6 +763,9 @@ string makeConstraintFn GetConstraints(view, ColumnAttrs.PrimaryKey).Select(c => (int?)c.Pos).SingleOrDefault(); } +/// +/// Represents a reducer method declaration in a module. +/// record ReducerDeclaration { public readonly string Name; @@ -640,26 +826,26 @@ public string GenerateClass() )})"; return $$""" - class {{Name}}: SpacetimeDB.Internal.IReducer { - {{MemberDeclaration.GenerateBsatnFields(Accessibility.Private, Args)}} + class {{Name}}: SpacetimeDB.Internal.IReducer { + {{MemberDeclaration.GenerateBsatnFields(Accessibility.Private, Args)}} - public SpacetimeDB.Internal.RawReducerDefV9 MakeReducerDef(SpacetimeDB.BSATN.ITypeRegistrar registrar) => new ( - nameof({{Name}}), - [{{MemberDeclaration.GenerateDefs(Args)}}], - {{Kind switch + public SpacetimeDB.Internal.RawReducerDefV9 MakeReducerDef(SpacetimeDB.BSATN.ITypeRegistrar registrar) => new ( + nameof({{Name}}), + [{{MemberDeclaration.GenerateDefs(Args)}}], + {{Kind switch { ReducerKind.Init => "SpacetimeDB.Internal.Lifecycle.Init", ReducerKind.ClientConnected => "SpacetimeDB.Internal.Lifecycle.OnConnect", ReducerKind.ClientDisconnected => "SpacetimeDB.Internal.Lifecycle.OnDisconnect", _ => "null" }}} - ); + ); - public void Invoke(BinaryReader reader, SpacetimeDB.Internal.IReducerContext ctx) { - {{invocation}}; - } - } - """; + public void Invoke(BinaryReader reader, SpacetimeDB.Internal.IReducerContext ctx) { + {{invocation}}; + } + } + """; } public Scope.Extensions GenerateSchedule() @@ -696,10 +882,7 @@ record ClientVisibilityFilterDeclaration { public readonly string FullName; - public string GlobalName - { - get => $"global::{FullName}"; - } + public string GlobalName => $"global::{FullName}"; public ClientVisibilityFilterDeclaration( GeneratorAttributeSyntaxContext context, @@ -729,6 +912,16 @@ DiagReporter diag [Generator] public class Module : IIncrementalGenerator { + /// + /// Collects distinct items from a source sequence, ensuring no duplicate export names exist. + /// + /// The type of items being collected + /// The category/type of items being collected (used for error messages) + /// The incremental generator context for reporting diagnostics + /// The source sequence of items to process + /// Function to get the export name for an item (used for deduplication) + /// Function to get the full name of an item (used for error messages) + /// An incremental value provider containing the distinct items private static IncrementalValueProvider> CollectDistinct( string kind, IncrementalGeneratorInitializationContext context, @@ -880,11 +1073,23 @@ public void Initialize(IncrementalGeneratorInitializationContext context) (f) => f.FullName ); + var columnDefaultValues = CollectDistinct( + "ColumnDefaultValues", + context, + tables + .SelectMany((t, ct) => t.GenerateDefaultValues()) + .WithTrackingName("SpacetimeDB.Table.GenerateDefaultValues"), + v => v.tableName + "_" + v.columnId, + v => v.tableName + "_" + v.columnId + ); + + // Register the generated source code with the compilation context as part of module publishing + // Once the compilation is complete, the generated code will be used to create tables and reducers in the database context.RegisterSourceOutput( - tableViews.Combine(addReducers).Combine(rlsFiltersArray), + tableViews.Combine(addReducers).Combine(rlsFiltersArray).Combine(columnDefaultValues), (context, tuple) => { - var ((tableViews, addReducers), rlsFilters) = tuple; + var (((tableViews, addReducers), rlsFilters), columnDefaultValues) = tuple; // Don't generate the FFI boilerplate if there are no tables or reducers. if (tableViews.Array.IsEmpty && addReducers.Array.IsEmpty) { @@ -940,6 +1145,8 @@ static class ModuleRegistration { #endif public static void Main() { SpacetimeDB.Internal.Module.SetReducerContextConstructor((identity, connectionId, random, time) => new SpacetimeDB.ReducerContext(identity, connectionId, random, time)); + var __memoryStream = new MemoryStream(); + var __writer = new BinaryWriter(__memoryStream); {{string.Join( "\n", @@ -955,6 +1162,18 @@ public static void Main() { "\n", rlsFilters.Select(f => $"SpacetimeDB.Internal.Module.RegisterClientVisibilityFilter({f.GlobalName});") )}} + {{string.Join( + "\n", + columnDefaultValues.Select(d => + "{\n" + +$"var value = new {d.BSATNTypeName}();\n" + +"__memoryStream.Position = 0;\n" + +"__memoryStream.SetLength(0);\n" + +$"value.Write(__writer, {d.value});\n" + +"var array = __memoryStream.ToArray();\n" + +$"SpacetimeDB.Internal.Module.RegisterTableDefaultValue(\"{d.tableName}\", {d.columnId}, array);" + + "\n}\n") + )}} } // Exports only work from the main assembly, so we need to generate forwarding methods. diff --git a/crates/bindings-csharp/Runtime/Attrs.cs b/crates/bindings-csharp/Runtime/Attrs.cs index dfb9baa0392..596da6c0764 100644 --- a/crates/bindings-csharp/Runtime/Attrs.cs +++ b/crates/bindings-csharp/Runtime/Attrs.cs @@ -12,6 +12,7 @@ public enum ColumnAttrs : byte Identity = Unique | AutoInc, PrimaryKey = Unique | 0b1000, PrimaryKeyAuto = PrimaryKey | AutoInc, + Default = 0b0001_0000, } [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] @@ -110,6 +111,45 @@ public sealed class UniqueAttribute : Internal.ColumnAttribute internal override Internal.ColumnAttrs Mask => Internal.ColumnAttrs.Unique; } + /// + /// Specifies a default value for a table column. + /// If a column is added to an existing table while republishing of a module, + /// the specified default value will be used to populate existing rows. + /// + /// + /// Updates existing instances of the class with the specified default value during republishing of a module. + /// + /// The default value for the column. + [AttributeUsage(AttributeTargets.Field)] + public sealed class DefaultAttribute(object value) : Internal.ColumnAttribute + { + /// + /// The default value for the column. + /// + public string Value + { + get + { + if (value is null) + { + return "null"; + } + if (value is bool) + { + return value.ToString()?.ToLower(); + } + var str = value.ToString(); + if (value is string) + { + str = $"\"{str}\""; + } + return str; + } + } + + internal override Internal.ColumnAttrs Mask => Internal.ColumnAttrs.Default; + } + public enum ReducerKind { /// diff --git a/crates/bindings-csharp/Runtime/Internal/Module.cs b/crates/bindings-csharp/Runtime/Internal/Module.cs index ecd079e1ad8..a87b27e4d2e 100644 --- a/crates/bindings-csharp/Runtime/Internal/Module.cs +++ b/crates/bindings-csharp/Runtime/Internal/Module.cs @@ -1,7 +1,9 @@ namespace SpacetimeDB.Internal; using System; +using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Text; using SpacetimeDB; using SpacetimeDB.BSATN; @@ -38,6 +40,16 @@ internal AlgebraicType.Ref RegisterType(Func RowLevelSecurity.Add(rls); + + internal void RegisterTableDefaultValue(string table, ushort colId, byte[] value) + { + var byteList = new List(value); + MiscExports.Add( + new RawMiscModuleExportV9.ColumnDefaultValue( + new RawColumnDefaultValueV9(table, colId, byteList) + ) + ); + } } public static class Module @@ -93,10 +105,8 @@ public static void RegisterReducer() public static void RegisterTable() where T : IStructuralReadWrite, new() - where View : ITableView, new() - { + where View : ITableView, new() => moduleDef.RegisterTable(View.MakeTableDesc(typeRegistrar)); - } public static void RegisterClientVisibilityFilter(Filter rlsFilter) { @@ -110,6 +120,9 @@ public static void RegisterClientVisibilityFilter(Filter rlsFilter) } } + public static void RegisterTableDefaultValue(string table, ushort colId, byte[] value) => + moduleDef.RegisterTableDefaultValue(table, colId, value); + private static byte[] Consume(this BytesSource source) { if (source == BytesSource.INVALID) diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/Program.cs b/sdks/csharp/examples~/regression-tests/republishing/client/Program.cs new file mode 100644 index 00000000000..c1b8a5871bd --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/client/Program.cs @@ -0,0 +1,151 @@ +/// Regression tests run with a live server. +/// To run these, run a local SpacetimeDB via `spacetime start`, +/// then in a separate terminal run `tools~/run-regression-tests.sh PATH_TO_SPACETIMEDB_REPO_CHECKOUT`. +/// This is done on CI in .github/workflows/test.yml. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using SpacetimeDB; +using SpacetimeDB.Types; + +const string HOST = "http://localhost:3000"; +const string DBNAME = "republish-test"; + +DbConnection ConnectToDB() +{ + DbConnection? conn = null; + conn = DbConnection.Builder() + .WithUri(HOST) + .WithModuleName(DBNAME) + .OnConnect(OnConnected) + .OnConnectError((err) => + { + throw err; + }) + .OnDisconnect((conn, err) => + { + if (err != null) + { + throw err; + } + else + { + throw new Exception("Unexpected disconnect"); + } + }) + .Build(); + return conn; +} + +uint waiting = 0; +bool applied = false; +SubscriptionHandle? handle = null; + +void OnConnected(DbConnection conn, Identity identity, string authToken) +{ + Log.Debug($"Connected to {DBNAME} on {HOST}"); + handle = conn.SubscriptionBuilder() + .OnApplied(OnSubscriptionApplied) + .OnError((ctx, err) => + { + throw err; + }) + .Subscribe(["SELECT * FROM ExampleData"]); +} + +void OnSubscriptionApplied(SubscriptionEventContext context) +{ + applied = true; + + // Do some operations that alter row state; + // we will check that everything is in sync in the callbacks for these reducer calls. + var TOLERANCE = 0.00001f; + foreach (var exampleData in context.Db.ExampleData.Iter()) + { + if (exampleData.TestPass == 1) + { + List errors = new List(); + // This row should have had values set by default Attributes + if (exampleData.DefaultString != "This is a default string") { errors.Add("DefaultString"); } + if (exampleData.DefaultBool != true) { errors.Add("DefaultBool"); } + if (exampleData.DefaultI8 != 2) { errors.Add("DefaultI8"); } + if (exampleData.DefaultU8 != 2) { errors.Add("DefaultU8"); } + if (exampleData.DefaultI16 != 2) { errors.Add("DefaultI16"); } + if (exampleData.DefaultU16 != 2) { errors.Add("DefaultU16"); } + if (exampleData.DefaultI32 != 2) { errors.Add("DefaultI32"); } + if (exampleData.DefaultU32 != 2) { errors.Add("DefaultU32"); } + if (exampleData.DefaultI64 != 2) { errors.Add("DefaultI64"); } + if (exampleData.DefaultU64 != 2) { errors.Add("DefaultU64"); } + if (exampleData.DefaultHex != 2) { errors.Add("DefaultHex"); } + if (exampleData.DefaultBin != 2) { errors.Add("DefaultBin"); } + if (Math.Abs(exampleData.DefaultF32 - 2.0f) > TOLERANCE) { errors.Add("DefaultF32"); } + if (Math.Abs(exampleData.DefaultF64 - 2.0) > TOLERANCE) { errors.Add("DefaultF64"); } + if (exampleData.DefaultEnum != MyEnum.SetByAttribute) { errors.Add("DefaultEnum"); } + if (exampleData.DefaultNull != null) { errors.Add("DefaultNull"); } + + if (errors.Count > 0) + { + var errorString = string.Join(", ", errors); + Log.Info($"ExampleData with key {exampleData.Primary}: Error: Key added during initial test pass, newly added rows {errorString} were not set by default attributes"); + } + else + { + Log.Info($"ExampleData with key {exampleData.Primary}: Success! Key added during initial test pass, newly added rows are all properly set by default attributes"); + } + } + else if (exampleData.TestPass == 2) + { + List errors = new List(); + // This row should have had values set by initialized values + if (exampleData.DefaultString != "") { errors.Add("DefaultString"); } + if (exampleData.DefaultBool != false) { errors.Add("DefaultBool"); } + if (exampleData.DefaultI8 != 1) { errors.Add("DefaultI8"); } + if (exampleData.DefaultU8 != 1) { errors.Add("DefaultU8"); } + if (exampleData.DefaultI16 != 1) { errors.Add("DefaultI16"); } + if (exampleData.DefaultU16 != 1) { errors.Add("DefaultU16"); } + if (exampleData.DefaultI32 != 1) { errors.Add("DefaultI32"); } + if (exampleData.DefaultU32 != 1) { errors.Add("DefaultU32"); } + if (exampleData.DefaultI64 != 1) { errors.Add("DefaultI64"); } + if (exampleData.DefaultU64 != 1) { errors.Add("DefaultU64"); } + if (exampleData.DefaultHex != 1) { errors.Add("DefaultHex"); } + if (exampleData.DefaultBin != 1) { errors.Add("DefaultBin"); } + if (Math.Abs(exampleData.DefaultF32 - 1.0f) > TOLERANCE) { errors.Add("DefaultF32"); } + if (Math.Abs(exampleData.DefaultF64 - 1.0) > TOLERANCE) { errors.Add("DefaultF64"); } + if (exampleData.DefaultEnum != MyEnum.SetByInitalization) { errors.Add("DefaultEnum"); } + if (exampleData.DefaultNull == null || exampleData.DefaultNull.X != 1) { errors.Add("DefaultNull"); } + + if (errors.Count > 0) + { + var errorString = string.Join(", ", errors); + Log.Info($"ExampleData with key {exampleData.Primary}: Error: Key added after republishing, newly added rows {errorString} were not set by initialized values"); + } + else + { + Log.Error($"ExampleData with key {exampleData.Primary}: Success! Key added after republishing, newly added rows are all properly set by initialized values"); + } + } + } + Log.Info("Evaluation of ExampleData in republishing test completed."); +} + +System.AppDomain.CurrentDomain.UnhandledException += (sender, args) => +{ + Log.Exception($"Unhandled exception: {sender} {args}"); + Environment.Exit(1); +}; +var db = ConnectToDB(); +Log.Info("Starting timer"); +const int TIMEOUT = 20; // seconds; +var start = DateTime.Now; +while (!applied || waiting > 0) +{ + db.FrameTick(); + Thread.Sleep(100); + if ((DateTime.Now - start).Seconds > TIMEOUT) + { + Log.Error($"Timeout, all events should have elapsed in {TIMEOUT} seconds!"); + Environment.Exit(1); + } +} +Log.Info("Success"); +Environment.Exit(0); \ No newline at end of file diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/client.csproj b/sdks/csharp/examples~/regression-tests/republishing/client/client.csproj new file mode 100644 index 00000000000..3afdea6e783 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/client/client.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/Reducers/Insert.g.cs b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/Reducers/Insert.g.cs new file mode 100644 index 00000000000..fbbdf52e78e --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/Reducers/Insert.g.cs @@ -0,0 +1,72 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types +{ + public sealed partial class RemoteReducers : RemoteBase + { + public delegate void InsertHandler(ReducerEventContext ctx, uint id); + public event InsertHandler? OnInsert; + + public void Insert(uint id) + { + conn.InternalCallReducer(new Reducer.Insert(id), this.SetCallReducerFlags.InsertFlags); + } + + public bool InvokeInsert(ReducerEventContext ctx, Reducer.Insert args) + { + if (OnInsert == null) + { + if (InternalOnUnhandledReducerError != null) + { + switch (ctx.Event.Status) + { + case Status.Failed(var reason): InternalOnUnhandledReducerError(ctx, new Exception(reason)); break; + case Status.OutOfEnergy(var _): InternalOnUnhandledReducerError(ctx, new Exception("out of energy")); break; + } + } + return false; + } + OnInsert( + ctx, + args.Id + ); + return true; + } + } + + public abstract partial class Reducer + { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class Insert : Reducer, IReducerArgs + { + [DataMember(Name = "id")] + public uint Id; + + public Insert(uint Id) + { + this.Id = Id; + } + + public Insert() + { + } + + string IReducerArgs.ReducerName => "Insert"; + } + } + + public sealed partial class SetReducerFlags + { + internal CallReducerFlags InsertFlags; + public void Insert(CallReducerFlags flags) => InsertFlags = flags; + } +} diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/SpacetimeDBClient.g.cs b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/SpacetimeDBClient.g.cs new file mode 100644 index 00000000000..81551e7d410 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/SpacetimeDBClient.g.cs @@ -0,0 +1,504 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +// This was generated using spacetimedb cli version 1.5.0 (commit 93dbce08003b093790af2d5f590c5e597a24bfdb). + +#nullable enable + +using System; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types +{ + public sealed partial class RemoteReducers : RemoteBase + { + internal RemoteReducers(DbConnection conn, SetReducerFlags flags) : base(conn) => SetCallReducerFlags = flags; + internal readonly SetReducerFlags SetCallReducerFlags; + internal event Action? InternalOnUnhandledReducerError; + } + + public sealed partial class RemoteTables : RemoteTablesBase + { + public RemoteTables(DbConnection conn) + { + AddTable(ExampleData = new(conn)); + } + } + + public sealed partial class SetReducerFlags { } + + public interface IRemoteDbContext : IDbContext + { + public event Action? OnUnhandledReducerError; + } + + public sealed class EventContext : IEventContext, IRemoteDbContext + { + private readonly DbConnection conn; + + /// + /// The event that caused this callback to run. + /// + public readonly Event Event; + + /// + /// Access to tables in the client cache, which stores a read-only replica of the remote database state. + /// + /// The returned DbView will have a method to access each table defined by the module. + /// + public RemoteTables Db => conn.Db; + /// + /// Access to reducers defined by the module. + /// + /// The returned RemoteReducers will have a method to invoke each reducer defined by the module, + /// plus methods for adding and removing callbacks on each of those reducers. + /// + public RemoteReducers Reducers => conn.Reducers; + /// + /// Access to setters for per-reducer flags. + /// + /// The returned SetReducerFlags will have a method to invoke, + /// for each reducer defined by the module, + /// which call-flags for the reducer can be set. + /// + public SetReducerFlags SetReducerFlags => conn.SetReducerFlags; + /// + /// Returns true if the connection is active, i.e. has not yet disconnected. + /// + public bool IsActive => conn.IsActive; + /// + /// Close the connection. + /// + /// Throws an error if the connection is already closed. + /// + public void Disconnect() + { + conn.Disconnect(); + } + /// + /// Start building a subscription. + /// + /// A builder-pattern constructor for subscribing to queries, + /// causing matching rows to be replicated into the client cache. + public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder(); + /// + /// Get the Identity of this connection. + /// + /// This method returns null if the connection was constructed anonymously + /// and we have not yet received our newly-generated Identity from the host. + /// + public Identity? Identity => conn.Identity; + /// + /// Get this connection's ConnectionId. + /// + public ConnectionId ConnectionId => conn.ConnectionId; + /// + /// Register a callback to be called when a reducer with no handler returns an error. + /// + public event Action? OnUnhandledReducerError + { + add => Reducers.InternalOnUnhandledReducerError += value; + remove => Reducers.InternalOnUnhandledReducerError -= value; + } + + internal EventContext(DbConnection conn, Event Event) + { + this.conn = conn; + this.Event = Event; + } + } + + public sealed class ReducerEventContext : IReducerEventContext, IRemoteDbContext + { + private readonly DbConnection conn; + /// + /// The reducer event that caused this callback to run. + /// + public readonly ReducerEvent Event; + + /// + /// Access to tables in the client cache, which stores a read-only replica of the remote database state. + /// + /// The returned DbView will have a method to access each table defined by the module. + /// + public RemoteTables Db => conn.Db; + /// + /// Access to reducers defined by the module. + /// + /// The returned RemoteReducers will have a method to invoke each reducer defined by the module, + /// plus methods for adding and removing callbacks on each of those reducers. + /// + public RemoteReducers Reducers => conn.Reducers; + /// + /// Access to setters for per-reducer flags. + /// + /// The returned SetReducerFlags will have a method to invoke, + /// for each reducer defined by the module, + /// which call-flags for the reducer can be set. + /// + public SetReducerFlags SetReducerFlags => conn.SetReducerFlags; + /// + /// Returns true if the connection is active, i.e. has not yet disconnected. + /// + public bool IsActive => conn.IsActive; + /// + /// Close the connection. + /// + /// Throws an error if the connection is already closed. + /// + public void Disconnect() + { + conn.Disconnect(); + } + /// + /// Start building a subscription. + /// + /// A builder-pattern constructor for subscribing to queries, + /// causing matching rows to be replicated into the client cache. + public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder(); + /// + /// Get the Identity of this connection. + /// + /// This method returns null if the connection was constructed anonymously + /// and we have not yet received our newly-generated Identity from the host. + /// + public Identity? Identity => conn.Identity; + /// + /// Get this connection's ConnectionId. + /// + public ConnectionId ConnectionId => conn.ConnectionId; + /// + /// Register a callback to be called when a reducer with no handler returns an error. + /// + public event Action? OnUnhandledReducerError + { + add => Reducers.InternalOnUnhandledReducerError += value; + remove => Reducers.InternalOnUnhandledReducerError -= value; + } + + internal ReducerEventContext(DbConnection conn, ReducerEvent reducerEvent) + { + this.conn = conn; + Event = reducerEvent; + } + } + + public sealed class ErrorContext : IErrorContext, IRemoteDbContext + { + private readonly DbConnection conn; + /// + /// The Exception that caused this error callback to be run. + /// + public readonly Exception Event; + Exception IErrorContext.Event + { + get + { + return Event; + } + } + + /// + /// Access to tables in the client cache, which stores a read-only replica of the remote database state. + /// + /// The returned DbView will have a method to access each table defined by the module. + /// + public RemoteTables Db => conn.Db; + /// + /// Access to reducers defined by the module. + /// + /// The returned RemoteReducers will have a method to invoke each reducer defined by the module, + /// plus methods for adding and removing callbacks on each of those reducers. + /// + public RemoteReducers Reducers => conn.Reducers; + /// + /// Access to setters for per-reducer flags. + /// + /// The returned SetReducerFlags will have a method to invoke, + /// for each reducer defined by the module, + /// which call-flags for the reducer can be set. + /// + public SetReducerFlags SetReducerFlags => conn.SetReducerFlags; + /// + /// Returns true if the connection is active, i.e. has not yet disconnected. + /// + public bool IsActive => conn.IsActive; + /// + /// Close the connection. + /// + /// Throws an error if the connection is already closed. + /// + public void Disconnect() + { + conn.Disconnect(); + } + /// + /// Start building a subscription. + /// + /// A builder-pattern constructor for subscribing to queries, + /// causing matching rows to be replicated into the client cache. + public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder(); + /// + /// Get the Identity of this connection. + /// + /// This method returns null if the connection was constructed anonymously + /// and we have not yet received our newly-generated Identity from the host. + /// + public Identity? Identity => conn.Identity; + /// + /// Get this connection's ConnectionId. + /// + public ConnectionId ConnectionId => conn.ConnectionId; + /// + /// Register a callback to be called when a reducer with no handler returns an error. + /// + public event Action? OnUnhandledReducerError + { + add => Reducers.InternalOnUnhandledReducerError += value; + remove => Reducers.InternalOnUnhandledReducerError -= value; + } + + internal ErrorContext(DbConnection conn, Exception error) + { + this.conn = conn; + Event = error; + } + } + + public sealed class SubscriptionEventContext : ISubscriptionEventContext, IRemoteDbContext + { + private readonly DbConnection conn; + + /// + /// Access to tables in the client cache, which stores a read-only replica of the remote database state. + /// + /// The returned DbView will have a method to access each table defined by the module. + /// + public RemoteTables Db => conn.Db; + /// + /// Access to reducers defined by the module. + /// + /// The returned RemoteReducers will have a method to invoke each reducer defined by the module, + /// plus methods for adding and removing callbacks on each of those reducers. + /// + public RemoteReducers Reducers => conn.Reducers; + /// + /// Access to setters for per-reducer flags. + /// + /// The returned SetReducerFlags will have a method to invoke, + /// for each reducer defined by the module, + /// which call-flags for the reducer can be set. + /// + public SetReducerFlags SetReducerFlags => conn.SetReducerFlags; + /// + /// Returns true if the connection is active, i.e. has not yet disconnected. + /// + public bool IsActive => conn.IsActive; + /// + /// Close the connection. + /// + /// Throws an error if the connection is already closed. + /// + public void Disconnect() + { + conn.Disconnect(); + } + /// + /// Start building a subscription. + /// + /// A builder-pattern constructor for subscribing to queries, + /// causing matching rows to be replicated into the client cache. + public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder(); + /// + /// Get the Identity of this connection. + /// + /// This method returns null if the connection was constructed anonymously + /// and we have not yet received our newly-generated Identity from the host. + /// + public Identity? Identity => conn.Identity; + /// + /// Get this connection's ConnectionId. + /// + public ConnectionId ConnectionId => conn.ConnectionId; + /// + /// Register a callback to be called when a reducer with no handler returns an error. + /// + public event Action? OnUnhandledReducerError + { + add => Reducers.InternalOnUnhandledReducerError += value; + remove => Reducers.InternalOnUnhandledReducerError -= value; + } + + internal SubscriptionEventContext(DbConnection conn) + { + this.conn = conn; + } + } + + /// + /// Builder-pattern constructor for subscription queries. + /// + public sealed class SubscriptionBuilder + { + private readonly IDbConnection conn; + + private event Action? Applied; + private event Action? Error; + + /// + /// Private API, use conn.SubscriptionBuilder() instead. + /// + public SubscriptionBuilder(IDbConnection conn) + { + this.conn = conn; + } + + /// + /// Register a callback to run when the subscription is applied. + /// + public SubscriptionBuilder OnApplied( + Action callback + ) + { + Applied += callback; + return this; + } + + /// + /// Register a callback to run when the subscription fails. + /// + /// Note that this callback may run either when attempting to apply the subscription, + /// in which case Self::on_applied will never run, + /// or later during the subscription's lifetime if the module's interface changes, + /// in which case Self::on_applied may have already run. + /// + public SubscriptionBuilder OnError( + Action callback + ) + { + Error += callback; + return this; + } + + /// + /// Subscribe to the following SQL queries. + /// + /// This method returns immediately, with the data not yet added to the DbConnection. + /// The provided callbacks will be invoked once the data is returned from the remote server. + /// Data from all the provided queries will be returned at the same time. + /// + /// See the SpacetimeDB SQL docs for more information on SQL syntax: + /// https://spacetimedb.com/docs/sql + /// + public SubscriptionHandle Subscribe( + string[] querySqls + ) => new(conn, Applied, Error, querySqls); + + /// + /// Subscribe to all rows from all tables. + /// + /// This method is intended as a convenience + /// for applications where client-side memory use and network bandwidth are not concerns. + /// Applications where these resources are a constraint + /// should register more precise queries via Self.Subscribe + /// in order to replicate only the subset of data which the client needs to function. + /// + /// This method should not be combined with Self.Subscribe on the same DbConnection. + /// A connection may either Self.Subscribe to particular queries, + /// or Self.SubscribeToAllTables, but not both. + /// Attempting to call Self.Subscribe + /// on a DbConnection that has previously used Self.SubscribeToAllTables, + /// or vice versa, may misbehave in any number of ways, + /// including dropping subscriptions, corrupting the client cache, or panicking. + /// + public void SubscribeToAllTables() + { + // Make sure we use the legacy handle constructor here, even though there's only 1 query. + // We drop the error handler, since it can't be called for legacy subscriptions. + new SubscriptionHandle( + conn, + Applied, + new string[] { "SELECT * FROM *" } + ); + } + } + + public sealed class SubscriptionHandle : SubscriptionHandleBase + { + /// + /// Internal API. Construct SubscriptionHandles using conn.SubscriptionBuilder. + /// + public SubscriptionHandle(IDbConnection conn, Action? onApplied, string[] querySqls) : base(conn, onApplied, querySqls) + { } + + /// + /// Internal API. Construct SubscriptionHandles using conn.SubscriptionBuilder. + /// + public SubscriptionHandle( + IDbConnection conn, + Action? onApplied, + Action? onError, + string[] querySqls + ) : base(conn, onApplied, onError, querySqls) + { } + } + + public abstract partial class Reducer + { + private Reducer() { } + } + + public sealed class DbConnection : DbConnectionBase + { + public override RemoteTables Db { get; } + public readonly RemoteReducers Reducers; + public readonly SetReducerFlags SetReducerFlags = new(); + + public DbConnection() + { + Db = new(this); + Reducers = new(this, SetReducerFlags); + } + + protected override Reducer ToReducer(TransactionUpdate update) + { + var encodedArgs = update.ReducerCall.Args; + return update.ReducerCall.ReducerName switch + { + "Insert" => BSATNHelpers.Decode(encodedArgs), + var reducer => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}") + }; + } + + protected override IEventContext ToEventContext(Event Event) => + new EventContext(this, Event); + + protected override IReducerEventContext ToReducerEventContext(ReducerEvent reducerEvent) => + new ReducerEventContext(this, reducerEvent); + + protected override ISubscriptionEventContext MakeSubscriptionEventContext() => + new SubscriptionEventContext(this); + + protected override IErrorContext ToErrorContext(Exception exception) => + new ErrorContext(this, exception); + + protected override bool Dispatch(IReducerEventContext context, Reducer reducer) + { + var eventContext = (ReducerEventContext)context; + return reducer switch + { + Reducer.Insert args => Reducers.InvokeInsert(eventContext, args), + _ => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}") + }; + } + + public SubscriptionBuilder SubscriptionBuilder() => new(this); + public event Action OnUnhandledReducerError + { + add => Reducers.InternalOnUnhandledReducerError += value; + remove => Reducers.InternalOnUnhandledReducerError -= value; + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/Tables/ExampleData.g.cs b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/Tables/ExampleData.g.cs new file mode 100644 index 00000000000..0ed621d0d69 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/Tables/ExampleData.g.cs @@ -0,0 +1,39 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.BSATN; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types +{ + public sealed partial class RemoteTables + { + public sealed class ExampleDataHandle : RemoteTableHandle + { + protected override string RemoteTableName => "ExampleData"; + + public sealed class PrimaryUniqueIndex : UniqueIndexBase + { + protected override uint GetKey(ExampleData row) => row.Primary; + + public PrimaryUniqueIndex(ExampleDataHandle table) : base(table) { } + } + + public readonly PrimaryUniqueIndex Primary; + + internal ExampleDataHandle(DbConnection conn) : base(conn) + { + Primary = new(this); + } + + protected override object GetPrimaryKey(ExampleData row) => row.Primary; + } + + public readonly ExampleDataHandle ExampleData; + } +} diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/Types/ExampleData.g.cs b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/Types/ExampleData.g.cs new file mode 100644 index 00000000000..2928c719a29 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/Types/ExampleData.g.cs @@ -0,0 +1,99 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types +{ + [SpacetimeDB.Type] + [DataContract] + public sealed partial class ExampleData + { + [DataMember(Name = "Primary")] + public uint Primary; + [DataMember(Name = "TestPass")] + public uint TestPass; + [DataMember(Name = "DefaultString")] + public string DefaultString; + [DataMember(Name = "DefaultBool")] + public bool DefaultBool; + [DataMember(Name = "DefaultI8")] + public sbyte DefaultI8; + [DataMember(Name = "DefaultU8")] + public byte DefaultU8; + [DataMember(Name = "DefaultI16")] + public short DefaultI16; + [DataMember(Name = "DefaultU16")] + public ushort DefaultU16; + [DataMember(Name = "DefaultI32")] + public int DefaultI32; + [DataMember(Name = "DefaultU32")] + public uint DefaultU32; + [DataMember(Name = "DefaultI64")] + public long DefaultI64; + [DataMember(Name = "DefaultU64")] + public ulong DefaultU64; + [DataMember(Name = "DefaultHex")] + public int DefaultHex; + [DataMember(Name = "DefaultBin")] + public int DefaultBin; + [DataMember(Name = "DefaultF32")] + public float DefaultF32; + [DataMember(Name = "DefaultF64")] + public double DefaultF64; + [DataMember(Name = "DefaultEnum")] + public MyEnum DefaultEnum; + [DataMember(Name = "DefaultNull")] + public MyStruct? DefaultNull; + + public ExampleData( + uint Primary, + uint TestPass, + string DefaultString, + bool DefaultBool, + sbyte DefaultI8, + byte DefaultU8, + short DefaultI16, + ushort DefaultU16, + int DefaultI32, + uint DefaultU32, + long DefaultI64, + ulong DefaultU64, + int DefaultHex, + int DefaultBin, + float DefaultF32, + double DefaultF64, + MyEnum DefaultEnum, + MyStruct? DefaultNull + ) + { + this.Primary = Primary; + this.TestPass = TestPass; + this.DefaultString = DefaultString; + this.DefaultBool = DefaultBool; + this.DefaultI8 = DefaultI8; + this.DefaultU8 = DefaultU8; + this.DefaultI16 = DefaultI16; + this.DefaultU16 = DefaultU16; + this.DefaultI32 = DefaultI32; + this.DefaultU32 = DefaultU32; + this.DefaultI64 = DefaultI64; + this.DefaultU64 = DefaultU64; + this.DefaultHex = DefaultHex; + this.DefaultBin = DefaultBin; + this.DefaultF32 = DefaultF32; + this.DefaultF64 = DefaultF64; + this.DefaultEnum = DefaultEnum; + this.DefaultNull = DefaultNull; + } + + public ExampleData() + { + this.DefaultString = ""; + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/Types/MyEnum.g.cs b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/Types/MyEnum.g.cs new file mode 100644 index 00000000000..8c8a18ab256 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/Types/MyEnum.g.cs @@ -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. + +#nullable enable + +using System; + +namespace SpacetimeDB.Types +{ + [SpacetimeDB.Type] + public enum MyEnum + { + Default, + SetByInitalization, + SetByAttribute, + } +} diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/Types/MyStruct.g.cs b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/Types/MyStruct.g.cs new file mode 100644 index 00000000000..f3a46d86dbd --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/Types/MyStruct.g.cs @@ -0,0 +1,28 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types +{ + [SpacetimeDB.Type] + [DataContract] + public sealed partial class MyStruct + { + [DataMember(Name = "x")] + public int X; + + public MyStruct(int X) + { + this.X = X; + } + + public MyStruct() + { + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/republishing/server-initial/.gitignore b/sdks/csharp/examples~/regression-tests/republishing/server-initial/.gitignore new file mode 100644 index 00000000000..1746e3269ed --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/server-initial/.gitignore @@ -0,0 +1,2 @@ +bin +obj diff --git a/sdks/csharp/examples~/regression-tests/republishing/server-initial/Lib.cs b/sdks/csharp/examples~/regression-tests/republishing/server-initial/Lib.cs new file mode 100644 index 00000000000..3604311bf26 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/server-initial/Lib.cs @@ -0,0 +1,19 @@ +using SpacetimeDB; + +public static partial class Module +{ + [SpacetimeDB.Table(Name = "ExampleData", Public = true)] + public partial struct ExampleData + { + [SpacetimeDB.PrimaryKey] + public uint Primary; + public uint TestPass; + } + + [SpacetimeDB.Reducer] + public static void Insert(ReducerContext ctx, uint id) + { + var exampleData = ctx.Db.ExampleData.Insert(new ExampleData { Primary = id, TestPass = 1 }); + Log.Info($"Inserted key {exampleData.Primary} on pass {exampleData.TestPass}"); + } +} diff --git a/sdks/csharp/examples~/regression-tests/republishing/server-initial/StdbModule.csproj b/sdks/csharp/examples~/regression-tests/republishing/server-initial/StdbModule.csproj new file mode 100644 index 00000000000..3d6a7699986 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/server-initial/StdbModule.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + wasi-wasm + enable + enable + + + + + + + diff --git a/sdks/csharp/examples~/regression-tests/republishing/server-initial/global.json b/sdks/csharp/examples~/regression-tests/republishing/server-initial/global.json new file mode 100644 index 00000000000..4e550c173fd --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/server-initial/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.400", + "rollForward": "latestMinor" + } +} diff --git a/sdks/csharp/examples~/regression-tests/republishing/server-republish/.gitignore b/sdks/csharp/examples~/regression-tests/republishing/server-republish/.gitignore new file mode 100644 index 00000000000..1746e3269ed --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/server-republish/.gitignore @@ -0,0 +1,2 @@ +bin +obj diff --git a/sdks/csharp/examples~/regression-tests/republishing/server-republish/Lib.cs b/sdks/csharp/examples~/regression-tests/republishing/server-republish/Lib.cs new file mode 100644 index 00000000000..b34557a7a08 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/server-republish/Lib.cs @@ -0,0 +1,58 @@ +using SpacetimeDB; + +public static partial class Module +{ + [SpacetimeDB.Table(Name = "ExampleData", Public = true)] + public partial struct ExampleData + { + [SpacetimeDB.PrimaryKey] + public uint Primary; + public uint TestPass; + [Default("This is a default string")] public string DefaultString = ""; + [Default(true)] public bool DefaultBool = false; + [Default((sbyte)2)] public sbyte DefaultI8 = 1; + [Default((byte)2)] public byte DefaultU8 = 1; + [Default((short)2)] public short DefaultI16 = 1; + [Default((ushort)2)] public ushort DefaultU16 = 1; + [Default(2)] public int DefaultI32 = 1; + [Default(2U)] public uint DefaultU32 = 1U; + [Default(2L)] public long DefaultI64 = 1L; + [Default(2UL)] public ulong DefaultU64 = 1UL; + [Default(0x02)] public int DefaultHex = 1; + [Default(0b00000010)] public int DefaultBin = 1; + [Default(2.0f)] public float DefaultF32 = 1.0f; + [Default(2.0)] public double DefaultF64 = 1.0; + [Default(MyEnum.SetByAttribute)] public MyEnum DefaultEnum = MyEnum.SetByInitalization; + [Default(null!)] public MyStruct? DefaultNull = new MyStruct(1); + + public ExampleData() + { + } + } + + [SpacetimeDB.Type] + public enum MyEnum + { + Default, + SetByInitalization, + SetByAttribute + } + + [SpacetimeDB.Type] + public partial struct MyStruct + { + public int x; + + public MyStruct(int x) + { + this.x = x; + } + } + + [SpacetimeDB.Reducer] + public static void Insert(ReducerContext ctx, uint id) + { + var exampleData = ctx.Db.ExampleData.Insert(new ExampleData { Primary = id, TestPass = 2 }); + Log.Info($"Inserted key {exampleData.Primary} on pass {exampleData.TestPass}"); + } +} diff --git a/sdks/csharp/examples~/regression-tests/republishing/server-republish/StdbModule.csproj b/sdks/csharp/examples~/regression-tests/republishing/server-republish/StdbModule.csproj new file mode 100644 index 00000000000..3d6a7699986 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/server-republish/StdbModule.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + wasi-wasm + enable + enable + + + + + + + diff --git a/sdks/csharp/examples~/regression-tests/republishing/server-republish/global.json b/sdks/csharp/examples~/regression-tests/republishing/server-republish/global.json new file mode 100644 index 00000000000..4e550c173fd --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/republishing/server-republish/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.400", + "rollForward": "latestMinor" + } +} diff --git a/sdks/csharp/tools~/gen-regression-tests.sh b/sdks/csharp/tools~/gen-regression-tests.sh index 37ab6336d29..7a9058efe08 100755 --- a/sdks/csharp/tools~/gen-regression-tests.sh +++ b/sdks/csharp/tools~/gen-regression-tests.sh @@ -8,3 +8,4 @@ SDK_PATH="$(realpath "$SDK_PATH")" cargo build --manifest-path "$STDB_PATH/crates/standalone/Cargo.toml" cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- generate -y -l csharp -o "$SDK_PATH/examples~/regression-tests/client/module_bindings" --project-path "$SDK_PATH/examples~/regression-tests/server" +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- generate -y -l csharp -o "$SDK_PATH/examples~/regression-tests/republishing/client/module_bindings" --project-path "$SDK_PATH/examples~/regression-tests/republishing/server-republish" diff --git a/sdks/csharp/tools~/run-regression-tests.sh b/sdks/csharp/tools~/run-regression-tests.sh index ea1c6534897..dea10178625 100644 --- a/sdks/csharp/tools~/run-regression-tests.sh +++ b/sdks/csharp/tools~/run-regression-tests.sh @@ -8,7 +8,23 @@ STDB_PATH="$1" SDK_PATH="$(dirname "$0")/.." SDK_PATH="$(realpath "$SDK_PATH")" +# Regenerate Bindings "$SDK_PATH/tools~/gen-regression-tests.sh" "$STDB_PATH" + +# Build and run SpacetimeDB server cargo build --manifest-path "$STDB_PATH/crates/standalone/Cargo.toml" + +# Publish module for btree test cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y -p "$SDK_PATH/examples~/regression-tests/server" btree-repro -cd "$SDK_PATH/examples~/regression-tests/client" && dotnet run -c Debug \ No newline at end of file + +# Publish module for republishing module test +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y -p "$SDK_PATH/examples~/regression-tests/republishing/server-initial" republish-test +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" call republish-test Insert 1 +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -p "$SDK_PATH/examples~/regression-tests/republishing/server-republish" --break-clients republish-test +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" call republish-test Insert 2 + +# Run client for btree test +cd "$SDK_PATH/examples~/regression-tests/client" && dotnet run -c Debug + +# Run client for republishing module test +cd "$SDK_PATH/examples~/regression-tests/republishing/client" && dotnet run -c Debug \ No newline at end of file