diff --git a/sdks/csharp/examples~/regression-tests/client/Program.cs b/sdks/csharp/examples~/regression-tests/client/Program.cs index 269de6d8890..fb854245589 100644 --- a/sdks/csharp/examples~/regression-tests/client/Program.cs +++ b/sdks/csharp/examples~/regression-tests/client/Program.cs @@ -3,6 +3,7 @@ /// 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; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; @@ -72,18 +73,27 @@ void OnConnected(DbConnection conn, Identity identity, string authToken) .AddQuery(qb => qb.From.Admins().Build()) .AddQuery(qb => qb.From.NullableVecView().Build()) .AddQuery(qb => qb.From.WhereTest().Where(c => c.Value.Gt(10)).Build()) - .AddQuery( - qb => qb.From.Player() + .AddQuery(qb => + qb.From.Player() .LeftSemijoin(qb.From.PlayerLevel(), (p, pl) => p.Id.Eq(pl.PlayerId)) .Build() ) - .AddQuery( - qb => qb.From.Player() + .AddQuery(qb => + qb.From.Player() .Where(c => c.Name.Eq("NewPlayer")) .RightSemijoin(qb.From.PlayerLevel(), (p, pl) => p.Id.Eq(pl.PlayerId)) .Where(c => c.Level.Eq(1UL)) .Build() ) + .AddQuery(qb => qb.From.UsersNamedAlice().Build()) + .AddQuery(qb => qb.From.UsersAge1865().Build()) + .AddQuery(qb => qb.From.UsersAge18Plus().Build()) + .AddQuery(qb => qb.From.UsersAgeUnder18().Build()) + .AddQuery(qb => qb.From.ScoresPlayer123().Build()) + .AddQuery(qb => qb.From.ScoresPlayer123Range().Build()) + .AddQuery(qb => qb.From.ScoresPlayer123Level5().Build()) + .AddQuery(qb => qb.From.User().Build()) + .AddQuery(qb => qb.From.Score().Build()) .Subscribe(); // If testing against Rust, the indexed parameter will need to be changed to: ulong indexed @@ -116,7 +126,13 @@ void OnConnected(DbConnection conn, Identity identity, string authToken) ValidateNullableVecView(ctx); }; - conn.Reducers.OnSetNullableVec += (ReducerEventContext ctx, uint id, bool hasPos, int x, int y) => + conn.Reducers.OnSetNullableVec += ( + ReducerEventContext ctx, + uint id, + bool hasPos, + int x, + int y + ) => { Log.Info("Got SetNullableVec callback"); waiting--; @@ -152,7 +168,10 @@ void OnConnected(DbConnection conn, Identity identity, string authToken) if (ctx.Event.Status is Status.Failed(var reason)) { Debug.Assert( - reason.Contains("Cannot serialize a null string", StringComparison.OrdinalIgnoreCase) + 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}" @@ -236,8 +255,14 @@ void ValidateNullableVecView( else { Debug.Assert(row1.Pos != null, "Expected NullableVecView row 1 Pos != null"); - Debug.Assert(row1.Pos.X == expectedX, $"Expected row1.Pos.X == {expectedX}, got {row1.Pos.X}"); - Debug.Assert(row1.Pos.Y == expectedY, $"Expected row1.Pos.Y == {expectedY}, got {row1.Pos.Y}"); + Debug.Assert( + row1.Pos.X == expectedX, + $"Expected row1.Pos.X == {expectedX}, got {row1.Pos.X}" + ); + Debug.Assert( + row1.Pos.Y == expectedY, + $"Expected row1.Pos.Y == {expectedY}, got {row1.Pos.Y}" + ); } } } @@ -248,9 +273,62 @@ void ValidateReducerErrorDoesNotContainStackTrace(Exception exception) exception.Message == THROW_ERROR_MESSAGE, $"Expected reducer error message '{THROW_ERROR_MESSAGE}', got '{exception.Message}'" ); - Debug.Assert(!exception.Message.Contains("\n"), "Reducer error message should not contain newline"); - Debug.Assert(!exception.Message.Contains("\r"), "Reducer error message should not contain newline"); - Debug.Assert(!exception.Message.Contains(" at "), "Reducer error message should not contain stack trace"); + Debug.Assert( + !exception.Message.Contains("\n"), + "Reducer error message should not contain newline" + ); + Debug.Assert( + !exception.Message.Contains("\r"), + "Reducer error message should not contain newline" + ); + Debug.Assert( + !exception.Message.Contains(" at "), + "Reducer error message should not contain stack trace" + ); +} + +void ValidateQueryingWithIndexesExamples(IRemoteDbContext conn) +{ + Log.Debug("Checking 'Querying with Indexes' documentation examples..."); + + var usersNamedAlice = conn.Db.UsersNamedAlice.Iter().Select(u => u.Name).ToList(); + Debug.Assert( + usersNamedAlice.Count == 1 && usersNamedAlice[0] == "Alice", + "Expected exactly one Alice in users_named_alice view" + ); + + var ages18To65 = conn.Db.UsersAge1865.Iter().Select(u => u.Name).ToHashSet(); + Debug.Assert( + ages18To65.SetEquals(new[] { "Alice", "Charlie" }), + "Expected Alice and Charlie in 18-65 age range" + ); + + var ages18OrOlder = conn.Db.UsersAge18Plus.Iter().Select(u => u.Name).ToHashSet(); + Debug.Assert( + ages18OrOlder.SetEquals(new[] { "Alice", "Charlie" }), + "Expected Alice and Charlie to be >= 18" + ); + + var youngerThan18 = conn.Db.UsersAgeUnder18.Iter().Select(u => u.Name).ToHashSet(); + Debug.Assert(youngerThan18.SetEquals(new[] { "Bob" }), "Expected Bob to be the only minor"); + + var player123Scores = conn.Db.ScoresPlayer123.Iter().ToList(); + Debug.Assert( + player123Scores.Count == 3, + $"Expected 3 scores for player 123, got {player123Scores.Count}" + ); + + var player123LevelRange = conn.Db.ScoresPlayer123Range.Iter().ToList(); + Debug.Assert( + player123LevelRange.Count == 3, + "Expected three scores for player 123 between levels 1 and 10 inclusive" + ); + + var player123Level5 = conn.Db.ScoresPlayer123Level5.Iter().ToList(); + Debug.Assert( + player123Level5.Count == 1 && player123Level5[0].Points == 5_000, + "Expected a single level-5 score worth 5,000 points for player 123" + ); } void ValidateWhereSubscription(IRemoteDbContext conn) @@ -261,8 +339,14 @@ void ValidateWhereSubscription(IRemoteDbContext conn) var rows = conn.Db.WhereTest.Iter().ToList(); Debug.Assert(rows.Count == 2, $"Expected 2 where_test rows, got {rows.Count}"); Debug.Assert(rows.All(r => r.Value > 10), "Expected all where_test.Value > 10"); - Debug.Assert(rows.Any(r => r.Id == 2 && r.Name == "high"), "Expected where_test row id=2 name=high"); - Debug.Assert(rows.Any(r => r.Id == 3 && r.Name == "alsohigh"), "Expected where_test row id=3 name=alsohigh"); + Debug.Assert( + rows.Any(r => r.Id == 2 && r.Name == "high"), + "Expected where_test row id=2 name=high" + ); + Debug.Assert( + rows.Any(r => r.Id == 3 && r.Name == "alsohigh"), + "Expected where_test row id=3 name=alsohigh" + ); } void ValidateSemijoinSubscriptions(IRemoteDbContext conn, Identity identity) @@ -271,14 +355,23 @@ void ValidateSemijoinSubscriptions(IRemoteDbContext conn, Identity identity) var players = conn.Db.Player.Iter().ToList(); Debug.Assert(players.Count == 1, $"Expected 1 player row, got {players.Count}"); - Debug.Assert(players[0].Identity == identity, "Expected player.Identity to match the connection identity"); - Debug.Assert(players[0].Name == "NewPlayer", $"Expected player.Name == NewPlayer, got {players[0].Name}"); + Debug.Assert( + players[0].Identity == identity, + "Expected player.Identity to match the connection identity" + ); + Debug.Assert( + players[0].Name == "NewPlayer", + $"Expected player.Name == NewPlayer, got {players[0].Name}" + ); var playerId = players[0].Id; var levels = conn.Db.PlayerLevel.Iter().ToList(); Debug.Assert(levels.Count == 1, $"Expected 1 player_level row, got {levels.Count}"); - Debug.Assert(levels[0].PlayerId == playerId, "Expected player_level.PlayerId to match the subscribed player id"); + Debug.Assert( + levels[0].PlayerId == playerId, + "Expected player_level.PlayerId to match the subscribed player id" + ); Debug.Assert(levels[0].Level == 1, $"Expected player_level.Level == 1, got {levels[0].Level}"); } @@ -309,7 +402,9 @@ void OnSubscriptionApplied(SubscriptionEventContext context) Log.Debug("Calling InsertResult"); waiting++; - context.Reducers.InsertResult(Result.Ok(new MyTable(new ReturnStruct(42, "magic")))); + context.Reducers.InsertResult( + Result.Ok(new MyTable(new ReturnStruct(42, "magic"))) + ); waiting++; context.Reducers.InsertResult(Result.Err("Fail")); @@ -319,15 +414,10 @@ void OnSubscriptionApplied(SubscriptionEventContext context) var logs = logRows.ToArray(); var expected = new[] { - new MyLog(Result.Ok( - new MyTable(new ReturnStruct(42, "magic")) - )), + new MyLog(Result.Ok(new MyTable(new ReturnStruct(42, "magic")))), new MyLog(Result.Err("Fail")), }; - Debug.Assert( - logs.SequenceEqual(expected), - "Logs did not match expected results" - ); + Debug.Assert(logs.SequenceEqual(expected), "Logs did not match expected results"); // RemoteQuery test Log.Debug("Calling RemoteQuery"); @@ -362,10 +452,15 @@ void OnSubscriptionApplied(SubscriptionEventContext context) $"context.Db.PlayersAtLevelOne.Count = {context.Db.PlayersAtLevelOne.Count}" ); Debug.Assert(context.Db.Admins != null, "context.Db.Admins != null"); - Debug.Assert(context.Db.Admins.Count > 0, $"context.Db.Admins.Count = {context.Db.Admins.Count}"); + Debug.Assert( + context.Db.Admins.Count > 0, + $"context.Db.Admins.Count = {context.Db.Admins.Count}" + ); ValidateNullableVecView(context, expectedHasPos: true, expectedX: 1, expectedY: 2); + ValidateQueryingWithIndexesExamples(context); + Log.Debug("Calling Iter on View"); var viewIterRows = context.Db.MyPlayer.Iter(); var expectedPlayer = new Player @@ -388,11 +483,16 @@ void OnSubscriptionApplied(SubscriptionEventContext context) Log.Debug("Calling Iter on View Admins"); var adminsIterRows = context.Db.Admins.Iter(); var expectedAdminNames = new HashSet { "Alice", "Charlie" }; - Log.Debug("Admins Iter count: " + (adminsIterRows != null ? adminsIterRows.Count().ToString() : "null")); + Log.Debug( + "Admins Iter count: " + + (adminsIterRows != null ? adminsIterRows.Count().ToString() : "null") + ); Debug.Assert(adminsIterRows != null && adminsIterRows.Any()); - Log.Debug("Validating Admins View row data " + - $"Expected Names={string.Join(", ", expectedAdminNames)} => " + - $"Actual Names={string.Join(", ", adminsIterRows.Select(a => a.Name))}"); + Log.Debug( + "Validating Admins View row data " + + $"Expected Names={string.Join(", ", expectedAdminNames)} => " + + $"Actual Names={string.Join(", ", adminsIterRows.Select(a => a.Name))}" + ); Debug.Assert(adminsIterRows.All(a => expectedAdminNames.Contains(a.Name))); Log.Debug("Calling RemoteQuery on View"); @@ -459,256 +559,383 @@ void OnSubscriptionApplied(SubscriptionEventContext context) // Procedures tests Log.Debug("Calling ReadMySchemaViaHttp"); waiting++; - context.Procedures.ReadMySchemaViaHttp((IProcedureEventContext ctx, ProcedureCallbackResult result) => - { - try - { - Debug.Assert(result.IsSuccess, $"ReadMySchemaViaHttp should succeed. Error received: {result.Error}"); - Debug.Assert(result.Value != null, "ReadMySchemaViaHttp should return a string"); - Debug.Assert(result.Value.StartsWith("OK "), $"Expected OK prefix, got: {result.Value}"); - Debug.Assert( - result.Value.Contains("example_data"), - $"Expected schema response to mention example_data, got: {result.Value}" - ); - } - finally + context.Procedures.ReadMySchemaViaHttp( + (IProcedureEventContext ctx, ProcedureCallbackResult result) => { - waiting--; + try + { + Debug.Assert( + result.IsSuccess, + $"ReadMySchemaViaHttp should succeed. Error received: {result.Error}" + ); + Debug.Assert(result.Value != null, "ReadMySchemaViaHttp should return a string"); + Debug.Assert( + result.Value.StartsWith("OK "), + $"Expected OK prefix, got: {result.Value}" + ); + Debug.Assert( + result.Value.Contains("example_data"), + $"Expected schema response to mention example_data, got: {result.Value}" + ); + } + finally + { + waiting--; + } } - }); + ); Log.Debug("Calling InvalidHttpRequest"); waiting++; - context.Procedures.InvalidHttpRequest((IProcedureEventContext ctx, ProcedureCallbackResult result) => - { - try + context.Procedures.InvalidHttpRequest( + (IProcedureEventContext ctx, ProcedureCallbackResult result) => { - Debug.Assert(result.IsSuccess, $"InvalidHttpRequest should succeed. Error received: {result.Error}"); - Debug.Assert(result.Value != null, "InvalidHttpRequest should return a string"); - Debug.Assert(result.Value.StartsWith("ERR "), $"Expected ERR prefix, got: {result.Value}"); - } - finally - { - waiting--; + try + { + Debug.Assert( + result.IsSuccess, + $"InvalidHttpRequest should succeed. Error received: {result.Error}" + ); + Debug.Assert(result.Value != null, "InvalidHttpRequest should return a string"); + Debug.Assert( + result.Value.StartsWith("ERR "), + $"Expected ERR prefix, got: {result.Value}" + ); + } + finally + { + waiting--; + } } - }); + ); Log.Debug("Calling InsertWithTxRollback"); waiting++; - context.Procedures.InsertWithTxRollback((IProcedureEventContext ctx, ProcedureCallbackResult result) => - { - if (result.IsSuccess) + context.Procedures.InsertWithTxRollback( + (IProcedureEventContext ctx, ProcedureCallbackResult result) => { - Debug.Assert(context.Db.MyTable.Count == 0, $"MyTable should remain empty after rollback. Count was {context.Db.MyTable.Count}"); - Log.Debug("Insert with transaction rollback succeeded"); - } - else - { - throw new Exception("Expected InsertWithTransactionRollback to fail, but it succeeded"); + if (result.IsSuccess) + { + Debug.Assert( + context.Db.MyTable.Count == 0, + $"MyTable should remain empty after rollback. Count was {context.Db.MyTable.Count}" + ); + Log.Debug("Insert with transaction rollback succeeded"); + } + else + { + throw new Exception( + "Expected InsertWithTransactionRollback to fail, but it succeeded" + ); + } + waiting--; } - waiting--; - }); + ); Log.Debug("Calling InsertWithTxRollbackResult"); waiting++; - context.Procedures.InsertWithTxRollbackResult((IProcedureEventContext ctx, ProcedureCallbackResult> result) => - { - if (result.IsSuccess) + context.Procedures.InsertWithTxRollbackResult( + ( + IProcedureEventContext ctx, + ProcedureCallbackResult> result + ) => { - Debug.Assert(context.Db.MyTable.Count == 0, $"MyTable should remain empty after rollback result. Count was {context.Db.MyTable.Count}"); - Log.Debug("Insert with transaction result rollback succeeded"); - } - else - { - throw new Exception("Expected InsertWithTxRollbackResult to fail, but it succeeded"); + if (result.IsSuccess) + { + Debug.Assert( + context.Db.MyTable.Count == 0, + $"MyTable should remain empty after rollback result. Count was {context.Db.MyTable.Count}" + ); + Log.Debug("Insert with transaction result rollback succeeded"); + } + else + { + throw new Exception( + "Expected InsertWithTxRollbackResult to fail, but it succeeded" + ); + } + waiting--; } - waiting--; - }); + ); Log.Debug("Calling InsertWithTxPanic"); waiting++; - context.Procedures.InsertWithTxPanic((IProcedureEventContext ctx, ProcedureCallbackResult result) => - { - try - { - Debug.Assert(result.IsSuccess, $"InsertWithTxPanic should succeed (exception is caught). Error received: {result.Error}"); - Debug.Assert(context.Db.MyTable.Count == 0, $"MyTable should remain empty after exception abort. Count was {context.Db.MyTable.Count}"); - } - finally + context.Procedures.InsertWithTxPanic( + (IProcedureEventContext ctx, ProcedureCallbackResult result) => { - waiting--; + try + { + Debug.Assert( + result.IsSuccess, + $"InsertWithTxPanic should succeed (exception is caught). Error received: {result.Error}" + ); + Debug.Assert( + context.Db.MyTable.Count == 0, + $"MyTable should remain empty after exception abort. Count was {context.Db.MyTable.Count}" + ); + } + finally + { + waiting--; + } } - }); + ); Log.Debug("Calling DanglingTxWarning"); waiting++; - context.Procedures.DanglingTxWarning((IProcedureEventContext ctx, ProcedureCallbackResult result) => - { - try - { - Debug.Assert(result.IsSuccess, $"DanglingTxWarning should succeed. Error received: {result.Error}"); - Debug.Assert(context.Db.MyTable.Count == 0, $"MyTable should remain empty after dangling tx auto-abort. Count was {context.Db.MyTable.Count}"); - // Note: We can't easily assert on the warning log from client-side, - // but the server-side AssertRowCount verifies the auto-abort behavior - } - finally + context.Procedures.DanglingTxWarning( + (IProcedureEventContext ctx, ProcedureCallbackResult result) => { - waiting--; + try + { + Debug.Assert( + result.IsSuccess, + $"DanglingTxWarning should succeed. Error received: {result.Error}" + ); + Debug.Assert( + context.Db.MyTable.Count == 0, + $"MyTable should remain empty after dangling tx auto-abort. Count was {context.Db.MyTable.Count}" + ); + // Note: We can't easily assert on the warning log from client-side, + // but the server-side AssertRowCount verifies the auto-abort behavior + } + finally + { + waiting--; + } } - }); + ); Log.Debug("Calling InsertWithTxCommit"); waiting++; - context.Procedures.InsertWithTxCommit((IProcedureEventContext ctx, ProcedureCallbackResult result) => - { - try + context.Procedures.InsertWithTxCommit( + (IProcedureEventContext ctx, ProcedureCallbackResult result) => { - Debug.Assert(result.IsSuccess, $"InsertWithTxCommit should succeed. Error received: {result.Error}"); - var expectedRow = new MyTable(new ReturnStruct(42, "magic")); - var row = context.Db.MyTable.Iter().FirstOrDefault(); - Debug.Assert(row != null); - Debug.Assert(row.Equals(expectedRow)); - Log.Debug("Insert with transaction commit succeeded"); - } - finally - { - waiting--; + try + { + Debug.Assert( + result.IsSuccess, + $"InsertWithTxCommit should succeed. Error received: {result.Error}" + ); + var expectedRow = new MyTable(new ReturnStruct(42, "magic")); + var row = context.Db.MyTable.Iter().FirstOrDefault(); + Debug.Assert(row != null); + Debug.Assert(row.Equals(expectedRow)); + Log.Debug("Insert with transaction commit succeeded"); + } + finally + { + waiting--; + } } - }); + ); Log.Debug("Calling InsertWithTxRetry"); waiting++; - context.Procedures.InsertWithTxRetry((IProcedureEventContext ctx, ProcedureCallbackResult result) => - { - try - { - Debug.Assert(result.IsSuccess, $"InsertWithTxRetry should succeed after retry. Error received: {result.Error}"); - } - catch (Exception ex) - { - Log.Exception(ex); - throw; - } - finally + context.Procedures.InsertWithTxRetry( + (IProcedureEventContext ctx, ProcedureCallbackResult result) => { - waiting--; + try + { + Debug.Assert( + result.IsSuccess, + $"InsertWithTxRetry should succeed after retry. Error received: {result.Error}" + ); + } + catch (Exception ex) + { + Log.Exception(ex); + throw; + } + finally + { + waiting--; + } } - }); + ); Log.Debug("Calling TxContextCapabilities"); waiting++; - context.Procedures.TxContextCapabilities((IProcedureEventContext ctx, ProcedureCallbackResult result) => - { - try - { - Debug.Assert(result.IsSuccess, $"TxContextCapabilities should succeed. Error received: {result.Error}"); - Debug.Assert(result.Value != null && result.Value.B.StartsWith("sender:"), $"Expected sender info, got {result.Value.B}"); - - // Verify the inserted row has the expected data - var rows = context.Db.MyTable.Iter().ToList(); - var timestampRow = rows.FirstOrDefault(r => r.Field.B.StartsWith("tx-test")); - Debug.Assert(timestampRow != null && timestampRow.Field.A == 200, $"Expected field.A == 200, got {timestampRow.Field.A}"); - Debug.Assert(timestampRow.Field.B == "tx-test", $"Expected field.B == 'tx-test', got {timestampRow.Field.B}"); - } - finally + context.Procedures.TxContextCapabilities( + (IProcedureEventContext ctx, ProcedureCallbackResult result) => { - waiting--; + try + { + Debug.Assert( + result.IsSuccess, + $"TxContextCapabilities should succeed. Error received: {result.Error}" + ); + Debug.Assert( + result.Value != null && result.Value.B.StartsWith("sender:"), + $"Expected sender info, got {result.Value.B}" + ); + + // Verify the inserted row has the expected data + var rows = context.Db.MyTable.Iter().ToList(); + var timestampRow = rows.FirstOrDefault(r => r.Field.B.StartsWith("tx-test")); + Debug.Assert( + timestampRow != null && timestampRow.Field.A == 200, + $"Expected field.A == 200, got {timestampRow.Field.A}" + ); + Debug.Assert( + timestampRow.Field.B == "tx-test", + $"Expected field.B == 'tx-test', got {timestampRow.Field.B}" + ); + } + finally + { + waiting--; + } } - }); + ); Log.Debug("Calling AuthenticationCapabilities"); waiting++; - context.Procedures.AuthenticationCapabilities((IProcedureEventContext ctx, ProcedureCallbackResult result) => - { - try - { - Debug.Assert(result.IsSuccess, $"AuthenticationCapabilities should succeed. Error received: {result.Error}"); - Debug.Assert(result.Value != null, "Should return a valid sender-derived value"); - Debug.Assert(result.Value.B.Contains("jwt:") || result.Value.B == "no-jwt", $"Should return JWT info, got {result.Value.B}"); - - // Verify the inserted row has authentication information - var rows = context.Db.MyTable.Iter().ToList(); - - var authRow = rows.FirstOrDefault(r => r.Field.B.StartsWith("auth:")); - Debug.Assert(authRow is not null, "Should have a row with auth data"); - Debug.Assert(authRow.Field.B.Contains("sender:"), "Auth row should contain sender info"); - Debug.Assert(authRow.Field.B.Contains("conn:"), "Auth row should contain connection info"); - } - finally + context.Procedures.AuthenticationCapabilities( + (IProcedureEventContext ctx, ProcedureCallbackResult result) => { - waiting--; + try + { + Debug.Assert( + result.IsSuccess, + $"AuthenticationCapabilities should succeed. Error received: {result.Error}" + ); + Debug.Assert(result.Value != null, "Should return a valid sender-derived value"); + Debug.Assert( + result.Value.B.Contains("jwt:") || result.Value.B == "no-jwt", + $"Should return JWT info, got {result.Value.B}" + ); + + // Verify the inserted row has authentication information + var rows = context.Db.MyTable.Iter().ToList(); + + var authRow = rows.FirstOrDefault(r => r.Field.B.StartsWith("auth:")); + Debug.Assert(authRow is not null, "Should have a row with auth data"); + Debug.Assert( + authRow.Field.B.Contains("sender:"), + "Auth row should contain sender info" + ); + Debug.Assert( + authRow.Field.B.Contains("conn:"), + "Auth row should contain connection info" + ); + } + finally + { + waiting--; + } } - }); + ); Log.Debug("Calling SubscriptionEventOffset"); waiting++; - context.Procedures.SubscriptionEventOffset((IProcedureEventContext ctx, ProcedureCallbackResult result) => - { - try - { - Debug.Assert(result.IsSuccess, $"SubscriptionEventOffset should succeed. Error received: {result.Error}"); - Debug.Assert(result.Value != null && result.Value.A == 999, $"Expected A == 999, got {result.Value.A}"); - Debug.Assert(result.Value.B.StartsWith("committed:"), $"Expected committed timestamp, got {result.Value.B}"); - - // Verify the inserted row has the expected offset test data - var rows = context.Db.MyTable.Iter().ToList(); - var offsetRow = rows.FirstOrDefault(r => r.Field.B.StartsWith("offset-test:")); - Debug.Assert(offsetRow is not null, "Should have a row with offset-test data"); - Debug.Assert(offsetRow.Field.A == 999, "Offset test row should have A == 999"); - - // Note: Transaction offset information is not directly accessible in ProcedureEvent, - // but this test verifies that the transaction was committed and subscription events were generated - // The presence of the new row in the subscription confirms the transaction offset was processed - } - finally + context.Procedures.SubscriptionEventOffset( + (IProcedureEventContext ctx, ProcedureCallbackResult result) => { - waiting--; + try + { + Debug.Assert( + result.IsSuccess, + $"SubscriptionEventOffset should succeed. Error received: {result.Error}" + ); + Debug.Assert( + result.Value != null && result.Value.A == 999, + $"Expected A == 999, got {result.Value.A}" + ); + Debug.Assert( + result.Value.B.StartsWith("committed:"), + $"Expected committed timestamp, got {result.Value.B}" + ); + + // Verify the inserted row has the expected offset test data + var rows = context.Db.MyTable.Iter().ToList(); + var offsetRow = rows.FirstOrDefault(r => r.Field.B.StartsWith("offset-test:")); + Debug.Assert(offsetRow is not null, "Should have a row with offset-test data"); + Debug.Assert(offsetRow.Field.A == 999, "Offset test row should have A == 999"); + + // Note: Transaction offset information is not directly accessible in ProcedureEvent, + // but this test verifies that the transaction was committed and subscription events were generated + // The presence of the new row in the subscription confirms the transaction offset was processed + } + finally + { + waiting--; + } } - }); + ); Log.Debug("Calling DocumentationGapChecks with valid parameters"); waiting++; - context.Procedures.DocumentationGapChecks(42, "test-input", (IProcedureEventContext ctx, ProcedureCallbackResult result) => - { - try + context.Procedures.DocumentationGapChecks( + 42, + "test-input", + (IProcedureEventContext ctx, ProcedureCallbackResult result) => { - Debug.Assert(result.IsSuccess, "DocumentationGapChecks should succeed with valid parameters"); - - // Expected: inputValue * 2 + inputText.Length = 42 * 2 + 10 = 94 - var expectedValue = 42u * 2 + (uint)"test-input".Length; // 84 + 10 = 94 - Debug.Assert(result.Value != null && result.Value.A == expectedValue, $"Expected A == {expectedValue}, got {result.Value.A}"); - Debug.Assert(result.Value.B.StartsWith("success:"), $"Expected success message, got {result.Value.B}"); - Debug.Assert(result.Value.B.Contains("test-input"), "Result should contain input text"); - - // Verify the inserted row has the expected documentation gap test data - var rows = context.Db.MyTable.Iter().ToList(); - var docGapRow = rows.FirstOrDefault(r => r.Field.B.StartsWith("doc-gap:")); - Debug.Assert(docGapRow is not null, "Should have a row with doc-gap data"); - Debug.Assert(docGapRow.Field.A == expectedValue, $"Doc gap row should have A == {expectedValue}"); - Debug.Assert(docGapRow.Field.B.Contains("test-input"), "Doc gap row should contain input text"); - } - finally - { - waiting--; + try + { + Debug.Assert( + result.IsSuccess, + "DocumentationGapChecks should succeed with valid parameters" + ); + + // Expected: inputValue * 2 + inputText.Length = 42 * 2 + 10 = 94 + var expectedValue = 42u * 2 + (uint)"test-input".Length; // 84 + 10 = 94 + Debug.Assert( + result.Value != null && result.Value.A == expectedValue, + $"Expected A == {expectedValue}, got {result.Value.A}" + ); + Debug.Assert( + result.Value.B.StartsWith("success:"), + $"Expected success message, got {result.Value.B}" + ); + Debug.Assert( + result.Value.B.Contains("test-input"), + "Result should contain input text" + ); + + // Verify the inserted row has the expected documentation gap test data + var rows = context.Db.MyTable.Iter().ToList(); + var docGapRow = rows.FirstOrDefault(r => r.Field.B.StartsWith("doc-gap:")); + Debug.Assert(docGapRow is not null, "Should have a row with doc-gap data"); + Debug.Assert( + docGapRow.Field.A == expectedValue, + $"Doc gap row should have A == {expectedValue}" + ); + Debug.Assert( + docGapRow.Field.B.Contains("test-input"), + "Doc gap row should contain input text" + ); + } + finally + { + waiting--; + } } - }); + ); // Test error handling with invalid parameters Log.Debug("Calling DocumentationGapChecks with invalid parameters (should fail)"); waiting++; - context.Procedures.DocumentationGapChecks(0, "", (IProcedureEventContext ctx, ProcedureCallbackResult result) => - { - try + context.Procedures.DocumentationGapChecks( + 0, + "", + (IProcedureEventContext ctx, ProcedureCallbackResult result) => { - Debug.Assert(!result.IsSuccess, "DocumentationGapChecks should fail with invalid parameters"); - // TODO: Testing against Rust, this returned a different error type "System.Exception". Decide if this is a bug or not. - //Debug.Assert(result.Error is ArgumentException, $"Expected ArgumentException, got {result.Error?.GetType()}"); - } - finally - { - waiting--; + try + { + Debug.Assert( + !result.IsSuccess, + "DocumentationGapChecks should fail with invalid parameters" + ); + // TODO: Testing against Rust, this returned a different error type "System.Exception". Decide if this is a bug or not. + //Debug.Assert(result.Error is ArgumentException, $"Expected ArgumentException, got {result.Error?.GetType()}"); + } + finally + { + waiting--; + } } - }); + ); // Now unsubscribe and check that the unsubscribing is actually applied. Log.Debug("Calling Unsubscribe"); diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs index 73ff6b5f765..b8c7b3a9224 100644 --- a/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs @@ -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.3 (commit fc255cbe2242a47d2552e1891d9363339eb3553e). +// This was generated using spacetimedb cli version 1.11.3 (commit 7cd565f410fd6ad504b4397c4a970300b92a1c1c). #nullable enable @@ -29,7 +29,6 @@ public sealed partial class RemoteTables : RemoteTablesBase public RemoteTables(DbConnection conn) { AddTable(Admins = new(conn)); - AddTable(User = new(conn)); AddTable(Account = new(conn)); AddTable(ExampleData = new(conn)); AddTable(MyAccount = new(conn)); @@ -45,6 +44,15 @@ public RemoteTables(DbConnection conn) AddTable(PlayerLevel = new(conn)); AddTable(PlayersAtLevelOne = new(conn)); AddTable(RetryLog = new(conn)); + AddTable(Score = new(conn)); + AddTable(ScoresPlayer123 = new(conn)); + AddTable(ScoresPlayer123Level5 = new(conn)); + AddTable(ScoresPlayer123Range = new(conn)); + AddTable(User = new(conn)); + AddTable(UsersAge1865 = new(conn)); + AddTable(UsersAge18Plus = new(conn)); + AddTable(UsersAgeUnder18 = new(conn)); + AddTable(UsersNamedAlice = new(conn)); AddTable(WhereTest = new(conn)); } } @@ -599,7 +607,6 @@ public sealed class QueryBuilder public sealed class From { public global::SpacetimeDB.Table Admins() => new("Admins", new AdminsCols("Admins"), new AdminsIxCols("Admins")); - public global::SpacetimeDB.Table User() => new("User", new UserCols("User"), new UserIxCols("User")); public global::SpacetimeDB.Table Account() => new("account", new AccountCols("account"), new AccountIxCols("account")); public global::SpacetimeDB.Table ExampleData() => new("example_data", new ExampleDataCols("example_data"), new ExampleDataIxCols("example_data")); public global::SpacetimeDB.Table MyAccount() => new("my_account", new MyAccountCols("my_account"), new MyAccountIxCols("my_account")); @@ -615,6 +622,15 @@ public sealed class From public global::SpacetimeDB.Table PlayerLevel() => new("player_level", new PlayerLevelCols("player_level"), new PlayerLevelIxCols("player_level")); public global::SpacetimeDB.Table PlayersAtLevelOne() => new("players_at_level_one", new PlayersAtLevelOneCols("players_at_level_one"), new PlayersAtLevelOneIxCols("players_at_level_one")); public global::SpacetimeDB.Table RetryLog() => new("retry_log", new RetryLogCols("retry_log"), new RetryLogIxCols("retry_log")); + public global::SpacetimeDB.Table Score() => new("score", new ScoreCols("score"), new ScoreIxCols("score")); + public global::SpacetimeDB.Table ScoresPlayer123() => new("scores_player_123", new ScoresPlayer123Cols("scores_player_123"), new ScoresPlayer123IxCols("scores_player_123")); + public global::SpacetimeDB.Table ScoresPlayer123Level5() => new("scores_player_123_level5", new ScoresPlayer123Level5Cols("scores_player_123_level5"), new ScoresPlayer123Level5IxCols("scores_player_123_level5")); + public global::SpacetimeDB.Table ScoresPlayer123Range() => new("scores_player_123_range", new ScoresPlayer123RangeCols("scores_player_123_range"), new ScoresPlayer123RangeIxCols("scores_player_123_range")); + public global::SpacetimeDB.Table User() => new("user", new UserCols("user"), new UserIxCols("user")); + public global::SpacetimeDB.Table UsersAge1865() => new("users_age_18_65", new UsersAge1865Cols("users_age_18_65"), new UsersAge1865IxCols("users_age_18_65")); + public global::SpacetimeDB.Table UsersAge18Plus() => new("users_age_18_plus", new UsersAge18PlusCols("users_age_18_plus"), new UsersAge18PlusIxCols("users_age_18_plus")); + public global::SpacetimeDB.Table UsersAgeUnder18() => new("users_age_under_18", new UsersAgeUnder18Cols("users_age_under_18"), new UsersAgeUnder18IxCols("users_age_under_18")); + public global::SpacetimeDB.Table UsersNamedAlice() => new("users_named_alice", new UsersNamedAliceCols("users_named_alice"), new UsersNamedAliceIxCols("users_named_alice")); public global::SpacetimeDB.Table WhereTest() => new("where_test", new WhereTestCols("where_test"), new WhereTestIxCols("where_test")); } diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/Admins.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/Admins.g.cs index cb8c81539cf..2fc34a86d44 100644 --- a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/Admins.g.cs +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/Admins.g.cs @@ -30,12 +30,14 @@ public sealed class AdminsCols public global::SpacetimeDB.Col Id { get; } public global::SpacetimeDB.Col Name { get; } public global::SpacetimeDB.Col IsAdmin { get; } + public global::SpacetimeDB.Col Age { get; } public AdminsCols(string tableName) { Id = new global::SpacetimeDB.Col(tableName, "Id"); Name = new global::SpacetimeDB.Col(tableName, "Name"); IsAdmin = new global::SpacetimeDB.Col(tableName, "IsAdmin"); + Age = new global::SpacetimeDB.Col(tableName, "Age"); } } diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/Score.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/Score.g.cs new file mode 100644 index 00000000000..7a65ae4bc44 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/Score.g.cs @@ -0,0 +1,63 @@ +// 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 ScoreHandle : RemoteTableHandle + { + protected override string RemoteTableName => "score"; + + public sealed class ByPlayerAndLevelIndex : BTreeIndexBase<(uint PlayerId, uint Level)> + { + protected override (uint PlayerId, uint Level) GetKey(Score row) => (row.PlayerId, row.Level); + + public ByPlayerAndLevelIndex(ScoreHandle table) : base(table) { } + } + + public readonly ByPlayerAndLevelIndex ByPlayerAndLevel; + + internal ScoreHandle(DbConnection conn) : base(conn) + { + ByPlayerAndLevel = new(this); + } + } + + public readonly ScoreHandle Score; + } + + public sealed class ScoreCols + { + public global::SpacetimeDB.Col PlayerId { get; } + public global::SpacetimeDB.Col Level { get; } + public global::SpacetimeDB.Col Points { get; } + + public ScoreCols(string tableName) + { + PlayerId = new global::SpacetimeDB.Col(tableName, "PlayerId"); + Level = new global::SpacetimeDB.Col(tableName, "Level"); + Points = new global::SpacetimeDB.Col(tableName, "Points"); + } + } + + public sealed class ScoreIxCols + { + public global::SpacetimeDB.IxCol PlayerId { get; } + public global::SpacetimeDB.IxCol Level { get; } + + public ScoreIxCols(string tableName) + { + PlayerId = new global::SpacetimeDB.IxCol(tableName, "PlayerId"); + Level = new global::SpacetimeDB.IxCol(tableName, "Level"); + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/ScoresPlayer123.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/ScoresPlayer123.g.cs new file mode 100644 index 00000000000..a3098687e75 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/ScoresPlayer123.g.cs @@ -0,0 +1,49 @@ +// 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 ScoresPlayer123Handle : RemoteTableHandle + { + protected override string RemoteTableName => "scores_player_123"; + + internal ScoresPlayer123Handle(DbConnection conn) : base(conn) + { + } + } + + public readonly ScoresPlayer123Handle ScoresPlayer123; + } + + public sealed class ScoresPlayer123Cols + { + public global::SpacetimeDB.Col PlayerId { get; } + public global::SpacetimeDB.Col Level { get; } + public global::SpacetimeDB.Col Points { get; } + + public ScoresPlayer123Cols(string tableName) + { + PlayerId = new global::SpacetimeDB.Col(tableName, "PlayerId"); + Level = new global::SpacetimeDB.Col(tableName, "Level"); + Points = new global::SpacetimeDB.Col(tableName, "Points"); + } + } + + public sealed class ScoresPlayer123IxCols + { + + public ScoresPlayer123IxCols(string tableName) + { + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/ScoresPlayer123Level5.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/ScoresPlayer123Level5.g.cs new file mode 100644 index 00000000000..4de2c6bc980 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/ScoresPlayer123Level5.g.cs @@ -0,0 +1,49 @@ +// 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 ScoresPlayer123Level5Handle : RemoteTableHandle + { + protected override string RemoteTableName => "scores_player_123_level5"; + + internal ScoresPlayer123Level5Handle(DbConnection conn) : base(conn) + { + } + } + + public readonly ScoresPlayer123Level5Handle ScoresPlayer123Level5; + } + + public sealed class ScoresPlayer123Level5Cols + { + public global::SpacetimeDB.Col PlayerId { get; } + public global::SpacetimeDB.Col Level { get; } + public global::SpacetimeDB.Col Points { get; } + + public ScoresPlayer123Level5Cols(string tableName) + { + PlayerId = new global::SpacetimeDB.Col(tableName, "PlayerId"); + Level = new global::SpacetimeDB.Col(tableName, "Level"); + Points = new global::SpacetimeDB.Col(tableName, "Points"); + } + } + + public sealed class ScoresPlayer123Level5IxCols + { + + public ScoresPlayer123Level5IxCols(string tableName) + { + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/ScoresPlayer123Range.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/ScoresPlayer123Range.g.cs new file mode 100644 index 00000000000..d2545b5edbe --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/ScoresPlayer123Range.g.cs @@ -0,0 +1,49 @@ +// 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 ScoresPlayer123RangeHandle : RemoteTableHandle + { + protected override string RemoteTableName => "scores_player_123_range"; + + internal ScoresPlayer123RangeHandle(DbConnection conn) : base(conn) + { + } + } + + public readonly ScoresPlayer123RangeHandle ScoresPlayer123Range; + } + + public sealed class ScoresPlayer123RangeCols + { + public global::SpacetimeDB.Col PlayerId { get; } + public global::SpacetimeDB.Col Level { get; } + public global::SpacetimeDB.Col Points { get; } + + public ScoresPlayer123RangeCols(string tableName) + { + PlayerId = new global::SpacetimeDB.Col(tableName, "PlayerId"); + Level = new global::SpacetimeDB.Col(tableName, "Level"); + Points = new global::SpacetimeDB.Col(tableName, "Points"); + } + } + + public sealed class ScoresPlayer123RangeIxCols + { + + public ScoresPlayer123RangeIxCols(string tableName) + { + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/User.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/User.g.cs index 614d79c363a..68ee45c4ed7 100644 --- a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/User.g.cs +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/User.g.cs @@ -15,7 +15,16 @@ public sealed partial class RemoteTables { public sealed class UserHandle : RemoteTableHandle { - protected override string RemoteTableName => "User"; + protected override string RemoteTableName => "user"; + + public sealed class AgeIndex : BTreeIndexBase + { + protected override byte GetKey(User row) => row.Age; + + public AgeIndex(UserHandle table) : base(table) { } + } + + public readonly AgeIndex Age; public sealed class IdUniqueIndex : UniqueIndexBase { @@ -35,10 +44,21 @@ public IsAdminIndex(UserHandle table) : base(table) { } public readonly IsAdminIndex IsAdmin; + public sealed class NameIndex : BTreeIndexBase + { + protected override string GetKey(User row) => row.Name; + + public NameIndex(UserHandle table) : base(table) { } + } + + public readonly NameIndex Name; + internal UserHandle(DbConnection conn) : base(conn) { + Age = new(this); Id = new(this); IsAdmin = new(this); + Name = new(this); } protected override object GetPrimaryKey(User row) => row.Id; @@ -52,24 +72,30 @@ public sealed class UserCols public global::SpacetimeDB.Col Id { get; } public global::SpacetimeDB.Col Name { get; } public global::SpacetimeDB.Col IsAdmin { get; } + public global::SpacetimeDB.Col Age { get; } public UserCols(string tableName) { Id = new global::SpacetimeDB.Col(tableName, "Id"); Name = new global::SpacetimeDB.Col(tableName, "Name"); IsAdmin = new global::SpacetimeDB.Col(tableName, "IsAdmin"); + Age = new global::SpacetimeDB.Col(tableName, "Age"); } } public sealed class UserIxCols { public global::SpacetimeDB.IxCol Id { get; } + public global::SpacetimeDB.IxCol Name { get; } public global::SpacetimeDB.IxCol IsAdmin { get; } + public global::SpacetimeDB.IxCol Age { get; } public UserIxCols(string tableName) { Id = new global::SpacetimeDB.IxCol(tableName, "Id"); + Name = new global::SpacetimeDB.IxCol(tableName, "Name"); IsAdmin = new global::SpacetimeDB.IxCol(tableName, "IsAdmin"); + Age = new global::SpacetimeDB.IxCol(tableName, "Age"); } } } diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/UsersAge1865.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/UsersAge1865.g.cs new file mode 100644 index 00000000000..78171d66a35 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/UsersAge1865.g.cs @@ -0,0 +1,51 @@ +// 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 UsersAge1865Handle : RemoteTableHandle + { + protected override string RemoteTableName => "users_age_18_65"; + + internal UsersAge1865Handle(DbConnection conn) : base(conn) + { + } + } + + public readonly UsersAge1865Handle UsersAge1865; + } + + public sealed class UsersAge1865Cols + { + public global::SpacetimeDB.Col Id { get; } + public global::SpacetimeDB.Col Name { get; } + public global::SpacetimeDB.Col IsAdmin { get; } + public global::SpacetimeDB.Col Age { get; } + + public UsersAge1865Cols(string tableName) + { + Id = new global::SpacetimeDB.Col(tableName, "Id"); + Name = new global::SpacetimeDB.Col(tableName, "Name"); + IsAdmin = new global::SpacetimeDB.Col(tableName, "IsAdmin"); + Age = new global::SpacetimeDB.Col(tableName, "Age"); + } + } + + public sealed class UsersAge1865IxCols + { + + public UsersAge1865IxCols(string tableName) + { + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/UsersAge18Plus.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/UsersAge18Plus.g.cs new file mode 100644 index 00000000000..87f6afb1d69 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/UsersAge18Plus.g.cs @@ -0,0 +1,51 @@ +// 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 UsersAge18PlusHandle : RemoteTableHandle + { + protected override string RemoteTableName => "users_age_18_plus"; + + internal UsersAge18PlusHandle(DbConnection conn) : base(conn) + { + } + } + + public readonly UsersAge18PlusHandle UsersAge18Plus; + } + + public sealed class UsersAge18PlusCols + { + public global::SpacetimeDB.Col Id { get; } + public global::SpacetimeDB.Col Name { get; } + public global::SpacetimeDB.Col IsAdmin { get; } + public global::SpacetimeDB.Col Age { get; } + + public UsersAge18PlusCols(string tableName) + { + Id = new global::SpacetimeDB.Col(tableName, "Id"); + Name = new global::SpacetimeDB.Col(tableName, "Name"); + IsAdmin = new global::SpacetimeDB.Col(tableName, "IsAdmin"); + Age = new global::SpacetimeDB.Col(tableName, "Age"); + } + } + + public sealed class UsersAge18PlusIxCols + { + + public UsersAge18PlusIxCols(string tableName) + { + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/UsersAgeUnder18.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/UsersAgeUnder18.g.cs new file mode 100644 index 00000000000..a397cf7f0a7 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/UsersAgeUnder18.g.cs @@ -0,0 +1,51 @@ +// 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 UsersAgeUnder18Handle : RemoteTableHandle + { + protected override string RemoteTableName => "users_age_under_18"; + + internal UsersAgeUnder18Handle(DbConnection conn) : base(conn) + { + } + } + + public readonly UsersAgeUnder18Handle UsersAgeUnder18; + } + + public sealed class UsersAgeUnder18Cols + { + public global::SpacetimeDB.Col Id { get; } + public global::SpacetimeDB.Col Name { get; } + public global::SpacetimeDB.Col IsAdmin { get; } + public global::SpacetimeDB.Col Age { get; } + + public UsersAgeUnder18Cols(string tableName) + { + Id = new global::SpacetimeDB.Col(tableName, "Id"); + Name = new global::SpacetimeDB.Col(tableName, "Name"); + IsAdmin = new global::SpacetimeDB.Col(tableName, "IsAdmin"); + Age = new global::SpacetimeDB.Col(tableName, "Age"); + } + } + + public sealed class UsersAgeUnder18IxCols + { + + public UsersAgeUnder18IxCols(string tableName) + { + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/UsersNamedAlice.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/UsersNamedAlice.g.cs new file mode 100644 index 00000000000..6805fd9aa0b --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/UsersNamedAlice.g.cs @@ -0,0 +1,51 @@ +// 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 UsersNamedAliceHandle : RemoteTableHandle + { + protected override string RemoteTableName => "users_named_alice"; + + internal UsersNamedAliceHandle(DbConnection conn) : base(conn) + { + } + } + + public readonly UsersNamedAliceHandle UsersNamedAlice; + } + + public sealed class UsersNamedAliceCols + { + public global::SpacetimeDB.Col Id { get; } + public global::SpacetimeDB.Col Name { get; } + public global::SpacetimeDB.Col IsAdmin { get; } + public global::SpacetimeDB.Col Age { get; } + + public UsersNamedAliceCols(string tableName) + { + Id = new global::SpacetimeDB.Col(tableName, "Id"); + Name = new global::SpacetimeDB.Col(tableName, "Name"); + IsAdmin = new global::SpacetimeDB.Col(tableName, "IsAdmin"); + Age = new global::SpacetimeDB.Col(tableName, "Age"); + } + } + + public sealed class UsersNamedAliceIxCols + { + + public UsersNamedAliceIxCols(string tableName) + { + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Types/Score.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Types/Score.g.cs new file mode 100644 index 00000000000..4b80e0d9bd8 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Types/Score.g.cs @@ -0,0 +1,38 @@ +// 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 Score + { + [DataMember(Name = "PlayerId")] + public uint PlayerId; + [DataMember(Name = "Level")] + public uint Level; + [DataMember(Name = "Points")] + public long Points; + + public Score( + uint PlayerId, + uint Level, + long Points + ) + { + this.PlayerId = PlayerId; + this.Level = Level; + this.Points = Points; + } + + public Score() + { + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Types/User.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Types/User.g.cs index a0a52f41013..825676f8193 100644 --- a/sdks/csharp/examples~/regression-tests/client/module_bindings/Types/User.g.cs +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Types/User.g.cs @@ -19,16 +19,20 @@ public sealed partial class User public string Name; [DataMember(Name = "IsAdmin")] public bool IsAdmin; + [DataMember(Name = "Age")] + public byte Age; public User( SpacetimeDB.Uuid Id, string Name, - bool IsAdmin + bool IsAdmin, + byte Age ) { this.Id = Id; this.Name = Name; this.IsAdmin = IsAdmin; + this.Age = Age; } public User() diff --git a/sdks/csharp/examples~/regression-tests/server/Lib.cs b/sdks/csharp/examples~/regression-tests/server/Lib.cs index bb63e977af4..a0d5600d39b 100644 --- a/sdks/csharp/examples~/regression-tests/server/Lib.cs +++ b/sdks/csharp/examples~/regression-tests/server/Lib.cs @@ -25,10 +25,7 @@ public ReturnStruct() } [SpacetimeDB.Type] -public partial record ReturnEnum : SpacetimeDB.TaggedEnum<( - uint A, - string B - )>; +public partial record ReturnEnum : SpacetimeDB.TaggedEnum<(uint A, string B)>; [SpacetimeDB.Type] public partial struct DbVector2 @@ -118,16 +115,29 @@ public partial struct PlayerAndLevel public ulong Level; } - [SpacetimeDB.Table(Name = "User", Public = true)] + [SpacetimeDB.Table(Name = "user", Public = true)] public partial struct User { [SpacetimeDB.PrimaryKey] public Uuid Id; + [SpacetimeDB.Index.BTree] public string Name; [SpacetimeDB.Index.BTree] public bool IsAdmin; + + [SpacetimeDB.Index.BTree] + public byte Age; + } + + [SpacetimeDB.Table(Name = "score", Public = true)] + [SpacetimeDB.Index.BTree(Name = "by_player_and_level", Columns = new[] { "PlayerId", "Level" })] + public partial struct Score + { + public uint PlayerId; + public uint Level; + public long Points; } [SpacetimeDB.Table(Name = "nullable_vec", Public = true)] @@ -192,7 +202,7 @@ public static List PlayersAtLevelOne(AnonymousViewContext ctx) Id = p.Id, Identity = p.Identity, Name = p.Name, - Level = player.Level + Level = player.Level, }; rows.Add(row); } @@ -204,13 +214,92 @@ public static List PlayersAtLevelOne(AnonymousViewContext ctx) public static List Admins(AnonymousViewContext ctx) { var rows = new List(); - foreach (var user in ctx.Db.User.IsAdmin.Filter(true)) + foreach (var user in ctx.Db.user.IsAdmin.Filter(true)) + { + rows.Add(user); + } + return rows; + } + + [SpacetimeDB.View(Name = "users_named_alice", Public = true)] + public static List UsersNamedAlice(AnonymousViewContext ctx) + { + var rows = new List(); + foreach (var user in ctx.Db.user.Name.Filter("Alice")) + { + rows.Add(user); + } + return rows; + } + + [SpacetimeDB.View(Name = "users_age_18_65", Public = true)] + public static List UsersAge1865(AnonymousViewContext ctx) + { + var rows = new List(); + foreach (var user in ctx.Db.user.Age.Filter(new Bound(18, 65))) + { + rows.Add(user); + } + return rows; + } + + [SpacetimeDB.View(Name = "users_age_18_plus", Public = true)] + public static List UsersAge18Plus(AnonymousViewContext ctx) + { + var rows = new List(); + foreach (var user in ctx.Db.user.Age.Filter(new Bound(18, byte.MaxValue))) + { + rows.Add(user); + } + return rows; + } + + [SpacetimeDB.View(Name = "users_age_under_18", Public = true)] + public static List UsersAgeUnder18(AnonymousViewContext ctx) + { + var rows = new List(); + foreach (var user in ctx.Db.user.Age.Filter(new Bound(byte.MinValue, 17))) { rows.Add(user); } return rows; } + [SpacetimeDB.View(Name = "scores_player_123", Public = true)] + public static List ScoresPlayer123(AnonymousViewContext ctx) + { + var rows = new List(); + foreach (var score in ctx.Db.score.by_player_and_level.Filter(123u)) + { + rows.Add(score); + } + return rows; + } + + [SpacetimeDB.View(Name = "scores_player_123_range", Public = true)] + public static List ScoresPlayer123Range(AnonymousViewContext ctx) + { + var rows = new List(); + foreach ( + var score in ctx.Db.score.by_player_and_level.Filter((123u, new Bound(1u, 10u))) + ) + { + rows.Add(score); + } + return rows; + } + + [SpacetimeDB.View(Name = "scores_player_123_level5", Public = true)] + public static List ScoresPlayer123Level5(AnonymousViewContext ctx) + { + var rows = new List(); + foreach (var score in ctx.Db.score.by_player_and_level.Filter((123u, 5u))) + { + rows.Add(score); + } + return rows; + } + [SpacetimeDB.View(Name = "nullable_vec_view", Public = true)] public static List NullableVecView(AnonymousViewContext ctx) { @@ -256,11 +345,7 @@ public static void InsertResult(ReducerContext ctx, Result msg) [SpacetimeDB.Reducer] public static void SetNullableVec(ReducerContext ctx, uint id, bool hasPos, int x, int y) { - var row = new NullableVec - { - Id = id, - Pos = hasPos ? new DbVector2 { X = x, Y = y } : null - }; + var row = new NullableVec { Id = id, Pos = hasPos ? new DbVector2 { X = x, Y = y } : null }; if (ctx.Db.nullable_vec.Id.Find(id) is null) { @@ -293,7 +378,14 @@ public static void InsertNullStringIntoNullable(ReducerContext ctx) [SpacetimeDB.Reducer] public static void InsertWhereTest(ReducerContext ctx, uint id, uint value, string name) { - ctx.Db.where_test.Insert(new WhereTest { Id = id, Value = value, Name = name }); + ctx.Db.where_test.Insert( + new WhereTest + { + Id = id, + Value = value, + Name = name, + } + ); } [Reducer(ReducerKind.ClientConnected)] @@ -320,43 +412,98 @@ public static void ClientConnected(ReducerContext ctx) if (ctx.Db.nullable_vec.Id.Find(1) is null) { - ctx.Db.nullable_vec.Insert(new NullableVec - { - Id = 1, - Pos = new DbVector2 { X = 1, Y = 2 }, - }); + ctx.Db.nullable_vec.Insert( + new NullableVec + { + Id = 1, + Pos = new DbVector2 { X = 1, Y = 2 }, + } + ); } if (ctx.Db.nullable_vec.Id.Find(2) is null) { - ctx.Db.nullable_vec.Insert(new NullableVec - { - Id = 2, - Pos = null, - }); + ctx.Db.nullable_vec.Insert(new NullableVec { Id = 2, Pos = null }); } - foreach (var (Name, IsAdmin) in new List<(string Name, bool IsAdmin)> + if (ctx.Db.user.Count == 0) + { + foreach ( + var (Name, IsAdmin, Age) in new List<(string Name, bool IsAdmin, byte Age)> + { + ("Alice", true, (byte)30), + ("Bob", false, (byte)16), + ("Charlie", true, (byte)22), + } + ) { - ("Alice", true), - ("Bob", false), - ("Charlie", true) - }) + ctx.Db.user.Insert( + new User + { + Id = ctx.NewUuidV7(), + Name = Name, + IsAdmin = IsAdmin, + Age = Age, + } + ); + } + } + + if (ctx.Db.score.Count == 0) { - ctx.Db.User.Insert(new User { Id = ctx.NewUuidV7(), Name = Name, IsAdmin = IsAdmin }); + foreach ( + var (PlayerId, Level, Points) in new List<(uint PlayerId, uint Level, long Points)> + { + (123u, 1u, 1_000), + (123u, 5u, 5_000), + (123u, 10u, 10_000), + (999u, 2u, 2_500), + } + ) + { + ctx.Db.score.Insert( + new Score + { + PlayerId = PlayerId, + Level = Level, + Points = Points, + } + ); + } } if (ctx.Db.where_test.Id.Find(1) is null) { - ctx.Db.where_test.Insert(new WhereTest { Id = 1, Value = 5, Name = "low" }); + ctx.Db.where_test.Insert( + new WhereTest + { + Id = 1, + Value = 5, + Name = "low", + } + ); } if (ctx.Db.where_test.Id.Find(2) is null) { - ctx.Db.where_test.Insert(new WhereTest { Id = 2, Value = 15, Name = "high" }); + ctx.Db.where_test.Insert( + new WhereTest + { + Id = 2, + Value = 15, + Name = "high", + } + ); } if (ctx.Db.where_test.Id.Find(3) is null) { - ctx.Db.where_test.Insert(new WhereTest { Id = 3, Value = 15, Name = "alsohigh" }); + ctx.Db.where_test.Insert( + new WhereTest + { + Id = 3, + Value = 15, + Name = "alsohigh", + } + ); } } @@ -444,10 +591,7 @@ public static void InsertWithTxCommit(ProcedureContext ctx) { ctx.WithTx(tx => { - tx.Db.my_table.Insert(new MyTable - { - Field = new ReturnStruct(a: 42, b: "magic"), - }); + tx.Db.my_table.Insert(new MyTable { Field = new ReturnStruct(a: 42, b: "magic") }); return new Unit(); }); @@ -459,10 +603,7 @@ public static void InsertWithTxRollback(ProcedureContext ctx) { var outcome = ctx.TryWithTx(tx => { - tx.Db.my_table.Insert(new MyTable - { - Field = new ReturnStruct(a: 42, b: "magic") - }); + tx.Db.my_table.Insert(new MyTable { Field = new ReturnStruct(a: 42, b: "magic") }); throw new InvalidOperationException("rollback"); }); @@ -478,10 +619,7 @@ public static Result InsertWithTxRollbackResult(ProcedureC { var outcome = ctx.TryWithTx(tx => { - tx.Db.my_table.Insert(new MyTable - { - Field = new ReturnStruct(a: 42, b: "magic") - }); + tx.Db.my_table.Insert(new MyTable { Field = new ReturnStruct(a: 42, b: "magic") }); throw new InvalidOperationException("rollback"); }); @@ -569,10 +707,9 @@ public static void InsertWithTxPanic(ProcedureContext ctx) ctx.WithTx(tx => { // Insert a row - tx.Db.my_table.Insert(new MyTable - { - Field = new ReturnStruct(a: 99, b: "panic-test") - }); + tx.Db.my_table.Insert( + new MyTable { Field = new ReturnStruct(a: 99, b: "panic-test") } + ); // Throw an exception to abort the transaction throw new InvalidOperationException("panic abort"); @@ -600,10 +737,9 @@ public static void DanglingTxWarning(ProcedureContext ctx) ctx.WithTx(tx => { // Insert a row - tx.Db.my_table.Insert(new MyTable - { - Field = new ReturnStruct(a: 123, b: "dangling") - }); + tx.Db.my_table.Insert( + new MyTable { Field = new ReturnStruct(a: 123, b: "dangling") } + ); // Simulate an unexpected system exception that might leave transaction in limbo // This should trigger the transaction cleanup/auto-abort mechanisms @@ -634,15 +770,14 @@ public static ReturnStruct TxContextCapabilities(ProcedureContext ctx) var initialCount = tx.Db.my_table.Count; // Test 2: Insert data and verify it's visible within the same transaction - tx.Db.my_table.Insert(new MyTable - { - Field = new ReturnStruct(a: 200, b: "tx-test") - }); + tx.Db.my_table.Insert(new MyTable { Field = new ReturnStruct(a: 200, b: "tx-test") }); var countAfterInsert = tx.Db.my_table.Count; if (countAfterInsert != initialCount + 1) { - throw new InvalidOperationException($"Expected count {initialCount + 1}, got {countAfterInsert}"); + throw new InvalidOperationException( + $"Expected count {initialCount + 1}, got {countAfterInsert}" + ); } // Test 3: Verify transaction context properties are accessible @@ -651,7 +786,9 @@ public static ReturnStruct TxContextCapabilities(ProcedureContext ctx) if (txSender.Equals(ctx.Sender) == false) { - throw new InvalidOperationException("Transaction sender should match procedure sender"); + throw new InvalidOperationException( + "Transaction sender should match procedure sender" + ); } // Test 4: Return data from within transaction @@ -666,7 +803,9 @@ public static ReturnStruct TxContextCapabilities(ProcedureContext ctx) var actualCount = tx.Db.my_table.Count; if (actualCount == 0) { - throw new InvalidOperationException("Expected at least 1 MyTable row but found none - transaction may not have committed"); + throw new InvalidOperationException( + "Expected at least 1 MyTable row but found none - transaction may not have committed" + ); } return 0; }); @@ -700,22 +839,27 @@ public static ReturnStruct AuthenticationCapabilities(ProcedureContext ctx) if (txSender.Equals(procSender) == false) { throw new InvalidOperationException( - $"Transaction sender {txSender} should match procedure sender {procSender}"); + $"Transaction sender {txSender} should match procedure sender {procSender}" + ); } if (txConnectionId.Equals(procConnectionId) == false) { throw new InvalidOperationException( - $"Transaction connectionId {txConnectionId} should match procedure connectionId {procConnectionId}"); + $"Transaction connectionId {txConnectionId} should match procedure connectionId {procConnectionId}" + ); } // Test 4: Insert data with authentication information - tx.Db.my_table.Insert(new MyTable - { - Field = new ReturnStruct( - a: (uint)(txSender.GetHashCode() & 0xFF), - b: $"auth:sender:{txSender}:conn:{txConnectionId}") - }); + tx.Db.my_table.Insert( + new MyTable + { + Field = new ReturnStruct( + a: (uint)(txSender.GetHashCode() & 0xFF), + b: $"auth:sender:{txSender}:conn:{txConnectionId}" + ), + } + ); // Test 5: Check JWT claims (if available) var jwtInfo = "no-jwt"; @@ -733,9 +877,7 @@ public static ReturnStruct AuthenticationCapabilities(ProcedureContext ctx) jwtInfo = "jwt:unavailable"; } - return new ReturnStruct( - a: (uint)(txSender.GetHashCode() & 0xFF), - b: jwtInfo); + return new ReturnStruct(a: (uint)(txSender.GetHashCode() & 0xFF), b: jwtInfo); }); return result; @@ -754,7 +896,8 @@ public static ReturnStruct SubscriptionEventOffset(ProcedureContext ctx) { Field = new ReturnStruct( a: 999, // Use a distinctive value to identify this test - b: $"offset-test:{tx.Timestamp.MicrosecondsSinceUnixEpoch}") + b: $"offset-test:{tx.Timestamp.MicrosecondsSinceUnixEpoch}" + ), }; tx.Db.my_table.Insert(testData); @@ -762,7 +905,8 @@ public static ReturnStruct SubscriptionEventOffset(ProcedureContext ctx) // Return data that can be used to correlate with subscription events return new ReturnStruct( a: 999, - b: $"committed:{tx.Timestamp.MicrosecondsSinceUnixEpoch}"); + b: $"committed:{tx.Timestamp.MicrosecondsSinceUnixEpoch}" + ); }); // At this point, the transaction should be committed and subscription events @@ -772,7 +916,11 @@ public static ReturnStruct SubscriptionEventOffset(ProcedureContext ctx) } [SpacetimeDB.Procedure] - public static ReturnStruct DocumentationGapChecks(ProcedureContext ctx, uint inputValue, string inputText) + public static ReturnStruct DocumentationGapChecks( + ProcedureContext ctx, + uint inputValue, + string inputText + ) { // This procedure tests various documentation gaps and edge cases // Test 1: Parameter handling - procedures can accept multiple parameters @@ -795,25 +943,27 @@ public static ReturnStruct DocumentationGapChecks(ProcedureContext ctx, uint inp if (count > 10) { // Don't insert if too many rows - return new ReturnStruct( - a: (uint)count, - b: $"skipped:too-many-rows:{count}"); + return new ReturnStruct(a: (uint)count, b: $"skipped:too-many-rows:{count}"); } // Test 4: Complex data manipulation var processedValue = inputValue * 2 + (uint)inputText.Length; - tx.Db.my_table.Insert(new MyTable - { - Field = new ReturnStruct( - a: processedValue, - b: $"doc-gap:{inputText}:processed:{processedValue}") - }); + tx.Db.my_table.Insert( + new MyTable + { + Field = new ReturnStruct( + a: processedValue, + b: $"doc-gap:{inputText}:processed:{processedValue}" + ), + } + ); // Test 5: Return computed results return new ReturnStruct( a: processedValue, - b: $"success:input:{inputText}:result:{processedValue}"); + b: $"success:input:{inputText}:result:{processedValue}" + ); }); // Test 6: Post-transaction validation