Merge branch 'jdetter/csharp-release-changes', remote-tracking branch 'origin' into jdetter/tmp3

This commit is contained in:
John Detter
2026-01-09 16:21:46 -06:00
28 changed files with 512 additions and 20 deletions
@@ -809,6 +809,32 @@ public static partial class BSATNRuntimeTests
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
}
[Fact]
public static void NonNullableStringSerializationRejectsNull()
{
var stream = new MemoryStream();
var writer = new BinaryWriter(stream);
var serializer = new BSATN.String();
var ex = Assert.Throws<ArgumentNullException>(() => serializer.Write(writer, null!));
Assert.Contains("nullable string", ex.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public static void NullableStringOptionRoundTripsNull()
{
var stream = new MemoryStream();
var writer = new BinaryWriter(stream);
var serializer = new BSATN.RefOption<string, BSATN.String>();
serializer.Write(writer, null);
stream.Seek(0, SeekOrigin.Begin);
var reader = new BinaryReader(stream);
var value = serializer.Read(reader);
Assert.Null(value);
}
[Type]
partial struct ContainsEnum
{
@@ -542,8 +542,18 @@ public readonly struct String : IReadWrite<string>
public string Read(BinaryReader reader) =>
Encoding.UTF8.GetString(ByteArray.Instance.Read(reader));
public void Write(BinaryWriter writer, string value) =>
public void Write(BinaryWriter writer, string value)
{
if (value is null)
{
throw new ArgumentNullException(
nameof(value),
"Cannot serialize a null string as BSATN String. To serialize a null string you must use a nullable string (string?) so it is encoded as a BSATN option."
);
}
ByteArray.Instance.Write(writer, Encoding.UTF8.GetBytes(value));
}
public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
new AlgebraicType.String(default);
@@ -53,10 +53,13 @@ internal static class Util
public static T Read<T>(ReadOnlySpan<byte> source, bool littleEndian)
where T : struct
{
Debug.Assert(
source.Length == Marshal.SizeOf<T>(),
$"Error while reading ${typeof(T).FullName}: expected source span to be {Marshal.SizeOf<T>()} bytes long, but was {source.Length} bytes."
);
var expectedSize = Marshal.SizeOf<T>();
if (source.Length != expectedSize)
{
throw new ArgumentException(
$"Error while reading {typeof(T).FullName}: expected source span to be {expectedSize} bytes long, but was {source.Length} bytes."
);
}
var result = MemoryMarshal.Read<T>(source);
@@ -166,8 +169,18 @@ public readonly record struct ConnectionId
/// </summary>
/// <param name="hex"></param>
/// <returns></returns>
public static ConnectionId? FromHexString(string hex) =>
FromBigEndian(Util.StringToByteArray(hex));
public static ConnectionId? FromHexString(string hex)
{
if (hex.Length != 32)
{
throw new ArgumentException(
$"Expected ConnectionId hex string to be 32 characters long, but was {hex.Length}.",
nameof(hex)
);
}
return FromBigEndian(Util.StringToByteArray(hex));
}
public static ConnectionId Random()
{
@@ -268,7 +281,18 @@ public readonly record struct Identity : IEquatable<Identity>, IComparable, ICom
/// </summary>
/// <param name="hex"></param>
/// <returns></returns>
public static Identity FromHexString(string hex) => FromBigEndian(Util.StringToByteArray(hex));
public static Identity FromHexString(string hex)
{
if (hex.Length != 64)
{
throw new ArgumentException(
$"Expected Identity hex string to be 64 characters long, but was {hex.Length}.",
nameof(hex)
);
}
return FromBigEndian(Util.StringToByteArray(hex));
}
// --- auto-generated ---
public readonly struct BSATN : IReadWrite<Identity>
+1
View File
@@ -0,0 +1 @@
release~
@@ -65,6 +65,8 @@ void OnConnected(DbConnection conn, Identity identity, string authToken)
"SELECT * FROM my_player",
"SELECT * FROM players_at_level_one",
"SELECT * FROM my_table",
"SELECT * FROM null_string_nonnullable",
"SELECT * FROM null_string_nullable",
"SELECT * FROM my_log",
"SELECT * FROM Admins",
"SELECT * FROM nullable_vec_view",
@@ -113,6 +115,56 @@ void OnConnected(DbConnection conn, Identity identity, string authToken)
ValidateNullableVecView(ctx);
}
};
conn.Reducers.OnInsertEmptyStringIntoNonNullable += (ReducerEventContext ctx) =>
{
Log.Info("Got InsertEmptyStringIntoNonNullable callback");
waiting--;
Debug.Assert(
ctx.Event.Status is Status.Committed,
$"InsertEmptyStringIntoNonNullable should commit, got {ctx.Event.Status}"
);
Debug.Assert(
ctx.Db.NullStringNonnullable.Iter().Any(r => r.Name == ""),
"Expected a row inserted into null_string_nonnullable with Name == \"\""
);
};
conn.Reducers.OnInsertNullStringIntoNonNullable += (ReducerEventContext ctx) =>
{
Log.Info("Got InsertNullStringIntoNonNullable callback");
waiting--;
if (ctx.Event.Status is Status.Failed(var reason))
{
Debug.Assert(
reason.Contains("Cannot serialize a null string", StringComparison.OrdinalIgnoreCase)
|| reason.Contains("BSATN", StringComparison.OrdinalIgnoreCase)
|| reason.Contains("nullable string", StringComparison.OrdinalIgnoreCase),
$"Expected a serialization-related failure message, got: {reason}"
);
}
else
{
throw new Exception(
$"InsertNullStringIntoNonNullable should fail, got status {ctx.Event.Status}"
);
}
};
conn.Reducers.OnInsertNullStringIntoNullable += (ReducerEventContext ctx) =>
{
Log.Info("Got InsertNullStringIntoNullable callback");
waiting--;
Debug.Assert(
ctx.Event.Status is Status.Committed,
$"InsertNullStringIntoNullable should commit, got {ctx.Event.Status}"
);
Debug.Assert(
ctx.Db.NullStringNullable.Iter().Any(r => r.Name == null),
"Expected a row inserted into null_string_nullable with Name == null"
);
};
}
const uint MAX_ID = 10;
@@ -336,6 +388,18 @@ void OnSubscriptionApplied(SubscriptionEventContext context)
waiting++;
context.Reducers.SetNullableVec(1, true, 7, 8);
Log.Debug("Calling InsertEmptyStringIntoNonNullable");
waiting++;
context.Reducers.InsertEmptyStringIntoNonNullable();
Log.Debug("Calling InsertNullStringIntoNonNullable (should fail)");
waiting++;
context.Reducers.InsertNullStringIntoNonNullable();
Log.Debug("Calling InsertNullStringIntoNullable");
waiting++;
context.Reducers.InsertNullStringIntoNullable();
// Procedures tests
Log.Debug("Calling ReadMySchemaViaHttp");
waiting++;
@@ -0,0 +1,59 @@
// 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 InsertEmptyStringIntoNonNullableHandler(ReducerEventContext ctx);
public event InsertEmptyStringIntoNonNullableHandler? OnInsertEmptyStringIntoNonNullable;
public void InsertEmptyStringIntoNonNullable()
{
conn.InternalCallReducer(new Reducer.InsertEmptyStringIntoNonNullable(), this.SetCallReducerFlags.InsertEmptyStringIntoNonNullableFlags);
}
public bool InvokeInsertEmptyStringIntoNonNullable(ReducerEventContext ctx, Reducer.InsertEmptyStringIntoNonNullable args)
{
if (OnInsertEmptyStringIntoNonNullable == 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;
}
OnInsertEmptyStringIntoNonNullable(
ctx
);
return true;
}
}
public abstract partial class Reducer
{
[SpacetimeDB.Type]
[DataContract]
public sealed partial class InsertEmptyStringIntoNonNullable : Reducer, IReducerArgs
{
string IReducerArgs.ReducerName => "InsertEmptyStringIntoNonNullable";
}
}
public sealed partial class SetReducerFlags
{
internal CallReducerFlags InsertEmptyStringIntoNonNullableFlags;
public void InsertEmptyStringIntoNonNullable(CallReducerFlags flags) => InsertEmptyStringIntoNonNullableFlags = flags;
}
}
@@ -0,0 +1,59 @@
// 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 InsertNullStringIntoNonNullableHandler(ReducerEventContext ctx);
public event InsertNullStringIntoNonNullableHandler? OnInsertNullStringIntoNonNullable;
public void InsertNullStringIntoNonNullable()
{
conn.InternalCallReducer(new Reducer.InsertNullStringIntoNonNullable(), this.SetCallReducerFlags.InsertNullStringIntoNonNullableFlags);
}
public bool InvokeInsertNullStringIntoNonNullable(ReducerEventContext ctx, Reducer.InsertNullStringIntoNonNullable args)
{
if (OnInsertNullStringIntoNonNullable == 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;
}
OnInsertNullStringIntoNonNullable(
ctx
);
return true;
}
}
public abstract partial class Reducer
{
[SpacetimeDB.Type]
[DataContract]
public sealed partial class InsertNullStringIntoNonNullable : Reducer, IReducerArgs
{
string IReducerArgs.ReducerName => "InsertNullStringIntoNonNullable";
}
}
public sealed partial class SetReducerFlags
{
internal CallReducerFlags InsertNullStringIntoNonNullableFlags;
public void InsertNullStringIntoNonNullable(CallReducerFlags flags) => InsertNullStringIntoNonNullableFlags = flags;
}
}
@@ -0,0 +1,59 @@
// 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 InsertNullStringIntoNullableHandler(ReducerEventContext ctx);
public event InsertNullStringIntoNullableHandler? OnInsertNullStringIntoNullable;
public void InsertNullStringIntoNullable()
{
conn.InternalCallReducer(new Reducer.InsertNullStringIntoNullable(), this.SetCallReducerFlags.InsertNullStringIntoNullableFlags);
}
public bool InvokeInsertNullStringIntoNullable(ReducerEventContext ctx, Reducer.InsertNullStringIntoNullable args)
{
if (OnInsertNullStringIntoNullable == 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;
}
OnInsertNullStringIntoNullable(
ctx
);
return true;
}
}
public abstract partial class Reducer
{
[SpacetimeDB.Type]
[DataContract]
public sealed partial class InsertNullStringIntoNullable : Reducer, IReducerArgs
{
string IReducerArgs.ReducerName => "InsertNullStringIntoNullable";
}
}
public sealed partial class SetReducerFlags
{
internal CallReducerFlags InsertNullStringIntoNullableFlags;
public void InsertNullStringIntoNullable(CallReducerFlags flags) => InsertNullStringIntoNullableFlags = flags;
}
}
@@ -1,7 +1,7 @@
// 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.11.1 (commit 26474240db517aff2f24b1b6b0aff90b0e3b9758).
// This was generated using spacetimedb cli version 1.11.1 (commit b00ba57ed047514aca886b89d52b44520a7ac43d).
#nullable enable
@@ -34,6 +34,8 @@ namespace SpacetimeDB.Types
AddTable(MyLog = new(conn));
AddTable(MyPlayer = new(conn));
AddTable(MyTable = new(conn));
AddTable(NullStringNonnullable = new(conn));
AddTable(NullStringNullable = new(conn));
AddTable(NullableVec = new(conn));
AddTable(NullableVecView = new(conn));
AddTable(Player = new(conn));
@@ -603,6 +605,9 @@ namespace SpacetimeDB.Types
"Add" => BSATNHelpers.Decode<Reducer.Add>(encodedArgs),
"ClientConnected" => BSATNHelpers.Decode<Reducer.ClientConnected>(encodedArgs),
"Delete" => BSATNHelpers.Decode<Reducer.Delete>(encodedArgs),
"InsertEmptyStringIntoNonNullable" => BSATNHelpers.Decode<Reducer.InsertEmptyStringIntoNonNullable>(encodedArgs),
"InsertNullStringIntoNonNullable" => BSATNHelpers.Decode<Reducer.InsertNullStringIntoNonNullable>(encodedArgs),
"InsertNullStringIntoNullable" => BSATNHelpers.Decode<Reducer.InsertNullStringIntoNullable>(encodedArgs),
"InsertResult" => BSATNHelpers.Decode<Reducer.InsertResult>(encodedArgs),
"SetNullableVec" => BSATNHelpers.Decode<Reducer.SetNullableVec>(encodedArgs),
"ThrowError" => BSATNHelpers.Decode<Reducer.ThrowError>(encodedArgs),
@@ -634,6 +639,9 @@ namespace SpacetimeDB.Types
Reducer.Add args => Reducers.InvokeAdd(eventContext, args),
Reducer.ClientConnected args => Reducers.InvokeClientConnected(eventContext, args),
Reducer.Delete args => Reducers.InvokeDelete(eventContext, args),
Reducer.InsertEmptyStringIntoNonNullable args => Reducers.InvokeInsertEmptyStringIntoNonNullable(eventContext, args),
Reducer.InsertNullStringIntoNonNullable args => Reducers.InvokeInsertNullStringIntoNonNullable(eventContext, args),
Reducer.InsertNullStringIntoNullable args => Reducers.InvokeInsertNullStringIntoNullable(eventContext, args),
Reducer.InsertResult args => Reducers.InvokeInsertResult(eventContext, args),
Reducer.SetNullableVec args => Reducers.InvokeSetNullableVec(eventContext, args),
Reducer.ThrowError args => Reducers.InvokeThrowError(eventContext, args),
@@ -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 NullStringNonnullableHandle : RemoteTableHandle<EventContext, NullStringNonNullable>
{
protected override string RemoteTableName => "null_string_nonnullable";
public sealed class IdUniqueIndex : UniqueIndexBase<ulong>
{
protected override ulong GetKey(NullStringNonNullable row) => row.Id;
public IdUniqueIndex(NullStringNonnullableHandle table) : base(table) { }
}
public readonly IdUniqueIndex Id;
internal NullStringNonnullableHandle(DbConnection conn) : base(conn)
{
Id = new(this);
}
protected override object GetPrimaryKey(NullStringNonNullable row) => row.Id;
}
public readonly NullStringNonnullableHandle NullStringNonnullable;
}
}
@@ -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 NullStringNullableHandle : RemoteTableHandle<EventContext, NullStringNullable>
{
protected override string RemoteTableName => "null_string_nullable";
public sealed class IdUniqueIndex : UniqueIndexBase<ulong>
{
protected override ulong GetKey(NullStringNullable row) => row.Id;
public IdUniqueIndex(NullStringNullableHandle table) : base(table) { }
}
public readonly IdUniqueIndex Id;
internal NullStringNullableHandle(DbConnection conn) : base(conn)
{
Id = new(this);
}
protected override object GetPrimaryKey(NullStringNullable row) => row.Id;
}
public readonly NullStringNullableHandle NullStringNullable;
}
}
@@ -0,0 +1,35 @@
// 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 NullStringNonNullable
{
[DataMember(Name = "Id")]
public ulong Id;
[DataMember(Name = "Name")]
public string Name;
public NullStringNonNullable(
ulong Id,
string Name
)
{
this.Id = Id;
this.Name = Name;
}
public NullStringNonNullable()
{
this.Name = "";
}
}
}
@@ -0,0 +1,34 @@
// 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 NullStringNullable
{
[DataMember(Name = "Id")]
public ulong Id;
[DataMember(Name = "Name")]
public string? Name;
public NullStringNullable(
ulong Id,
string? Name
)
{
this.Id = Id;
this.Name = Name;
}
public NullStringNullable()
{
}
}
}
@@ -113,6 +113,26 @@ public static partial class Module
public DbVector2? Pos;
}
[SpacetimeDB.Table(Name = "null_string_nonnullable", Public = true)]
public partial struct NullStringNonNullable
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
public ulong Id;
public string Name;
}
[SpacetimeDB.Table(Name = "null_string_nullable", Public = true)]
public partial struct NullStringNullable
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
public ulong Id;
public string? Name;
}
// At-most-one row: return T?
[SpacetimeDB.View(Name = "my_player", Public = true)]
public static Player? MyPlayer(ViewContext ctx)
@@ -214,6 +234,24 @@ public static partial class Module
}
}
[SpacetimeDB.Reducer]
public static void InsertEmptyStringIntoNonNullable(ReducerContext ctx)
{
ctx.Db.null_string_nonnullable.Insert(new NullStringNonNullable { Name = "" });
}
[SpacetimeDB.Reducer]
public static void InsertNullStringIntoNonNullable(ReducerContext ctx)
{
ctx.Db.null_string_nonnullable.Insert(new NullStringNonNullable { Name = null! });
}
[SpacetimeDB.Reducer]
public static void InsertNullStringIntoNullable(ReducerContext ctx)
{
ctx.Db.null_string_nullable.Insert(new NullStringNullable { Name = null });
}
[Reducer(ReducerKind.ClientConnected)]
public static void ClientConnected(ReducerContext ctx)
{
-11
View File
@@ -1,11 +0,0 @@
# Ignore most of NuGet package structure, except DLLs which are required by Unity.
/*/*/*
!/*/*/analyzers
!/*/*/analyzers.meta
!/*/*/lib
!/*/*/lib.meta
# Ignore XML documentation metadata from packages too.
*.xml
@@ -0,0 +1,8 @@
.nupkg.metadata
.signature.p7s
LICENSE
README.md
logo.png
spacetimedb.bsatn.runtime.*.nupkg*
spacetimedb.bsatn.runtime.nuspec
*.xml