diff --git a/crates/bindings-csharp/BSATN.Runtime/BSATN/AlgebraicType.cs b/crates/bindings-csharp/BSATN.Runtime/BSATN/AlgebraicType.cs index b88ae42dcb4..43662c057a0 100644 --- a/crates/bindings-csharp/BSATN.Runtime/BSATN/AlgebraicType.cs +++ b/crates/bindings-csharp/BSATN.Runtime/BSATN/AlgebraicType.cs @@ -39,6 +39,7 @@ Unit F64 )> { public static readonly AlgebraicType Unit = new Product([]); + public const string QueryBuilderProductTypeTag = "__query__"; // Special AlgebraicType that can be recognised by the SpacetimeDB `generate` CLI as an Option. internal static AlgebraicType MakeOption(AlgebraicType someType) => @@ -47,4 +48,8 @@ internal static AlgebraicType MakeOption(AlgebraicType someType) => // Special AlgebraicType that can be recognised by the SpacetimeDB `generate` CLI as a Result. internal static AlgebraicType MakeResult(AlgebraicType okType, AlgebraicType errType) => new Sum([new("ok", okType), new("err", errType)]); + + // Special AlgebraicType that can be recognised by the SpacetimeDB `generate` CLI as Query. + public static AlgebraicType MakeQueryBuilderProductType(Ref rowProductTypeRef) => + new Product([new(QueryBuilderProductTypeTag, rowProductTypeRef)]); } 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 c519fb9d7e0..260b42c3098 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 @@ -1636,10 +1636,9 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar IsPublic: true, IsAnonymous: false, Params: [], - ReturnType: new SpacetimeDB.BSATN.ValueOption< - PublicTable, - PublicTable.BSATN - >().GetAlgebraicType(registrar) + ReturnType: global::SpacetimeDB.BSATN.AlgebraicType.MakeQueryBuilderProductType( + new PublicTable.BSATN().GetAlgebraicType(registrar) + ) ); public byte[] Invoke( diff --git a/crates/bindings-csharp/Codegen/Module.cs b/crates/bindings-csharp/Codegen/Module.cs index e9c0d0522e5..111503f0472 100644 --- a/crates/bindings-csharp/Codegen/Module.cs +++ b/crates/bindings-csharp/Codegen/Module.cs @@ -1122,6 +1122,7 @@ record ViewDeclaration public readonly bool IsPublic; public readonly bool ReturnsQuery; public readonly TypeUse ReturnType; + public readonly TypeUse? QueryRowType; public readonly EquatableArray Parameters; public readonly Scope Scope; @@ -1186,15 +1187,12 @@ method.ReturnType is INamedTypeSymbol { ReturnsQuery = true; var rowType = TypeUse.Parse(method, queryRowType, diag); - var optType = queryRowType.IsValueType - ? "SpacetimeDB.BSATN.ValueOption" - : "SpacetimeDB.BSATN.RefOption"; - var opt = $"{optType}<{rowType.Name}, {rowType.BSATNName}>"; - // Match Rust semantics: Query is described as a nullable row (T?). - ReturnType = new ReferenceUse(opt, opt); + QueryRowType = rowType; + ReturnType = rowType; } else { + QueryRowType = null; ReturnType = TypeUse.Parse(method, method.ReturnType, diag); } Scope = new Scope(methodSyntax.Parent as MemberDeclarationSyntax); @@ -1211,9 +1209,10 @@ method.ReturnType is INamedTypeSymbol diag.Report(ErrorDescriptor.ViewContextParam, methodSyntax); } - // Validate return type: must be List or T? + // Validate return type: must be List, T?, or IQuery. if ( - !ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.ValueOption") + !ReturnsQuery + && !ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.ValueOption") && !ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.RefOption") && !ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.List") ) @@ -1229,17 +1228,22 @@ method.ReturnType is INamedTypeSymbol ); } - public string GenerateViewDef(uint Index) => - $$$""" + public string GenerateViewDef(uint Index) + { + var returnTypeExpr = ReturnsQuery + ? $"global::SpacetimeDB.BSATN.AlgebraicType.MakeQueryBuilderProductType(new {QueryRowType!.BSATNName}().GetAlgebraicType(registrar))" + : $"new {ReturnType.BSATNName}().GetAlgebraicType(registrar)"; + return $$$""" new global::SpacetimeDB.Internal.RawViewDefV10( SourceName: "{{{Name}}}", Index: {{{Index}}}, IsPublic: {{{IsPublic.ToString().ToLower()}}}, IsAnonymous: {{{IsAnonymous.ToString().ToLower()}}}, Params: [{{{MemberDeclaration.GenerateDefs(Parameters)}}}], - ReturnType: new {{{ReturnType.BSATNName}}}().GetAlgebraicType(registrar) + ReturnType: {{{returnTypeExpr}}} ); """; + } /// /// Generates the class responsible for evaluating a view. diff --git a/crates/codegen/src/csharp.rs b/crates/codegen/src/csharp.rs index e449f703181..76907815898 100644 --- a/crates/codegen/src/csharp.rs +++ b/crates/codegen/src/csharp.rs @@ -653,6 +653,13 @@ impl Lang for Csharp<'_> { } } } + for (columns, constraints) in schema.backcompat_column_constraints() { + if constraints.has_indexed() || constraints.has_unique() || constraints.has_primary_key() { + for col_pos in columns.iter() { + ix_col_positions.insert(col_pos.idx()); + } + } + } writeln!(output, "public sealed class {cols_owner_name}Cols"); indented_block(&mut output, |output| { diff --git a/modules/sdk-test-view-pk-cs/.gitignore b/modules/sdk-test-view-pk-cs/.gitignore new file mode 100644 index 00000000000..1746e3269ed --- /dev/null +++ b/modules/sdk-test-view-pk-cs/.gitignore @@ -0,0 +1,2 @@ +bin +obj diff --git a/modules/sdk-test-view-pk-cs/Lib.cs b/modules/sdk-test-view-pk-cs/Lib.cs new file mode 100644 index 00000000000..5fa58de076b --- /dev/null +++ b/modules/sdk-test-view-pk-cs/Lib.cs @@ -0,0 +1,92 @@ +namespace SpacetimeDB.Sdk.Test.ViewPk; + +using SpacetimeDB; + +public static partial class Module +{ + [SpacetimeDB.Table(Accessor = "view_pk_player", Public = true)] + public partial struct ViewPkPlayer + { + [SpacetimeDB.PrimaryKey] + public ulong id; + public string name; + } + + [SpacetimeDB.Table(Accessor = "view_pk_membership", Public = true)] + public partial struct ViewPkMembership + { + [SpacetimeDB.PrimaryKey] + public ulong id; + + [SpacetimeDB.Index.BTree] + public ulong player_id; + } + + [SpacetimeDB.Table(Accessor = "view_pk_membership_secondary", Public = true)] + public partial struct ViewPkMembershipSecondary + { + [SpacetimeDB.PrimaryKey] + public ulong id; + + [SpacetimeDB.Index.BTree] + public ulong player_id; + } + + [SpacetimeDB.Reducer] + public static void insert_view_pk_player(ReducerContext ctx, ulong id, string name) + { + ctx.Db.view_pk_player.Insert(new ViewPkPlayer { id = id, name = name }); + } + + [SpacetimeDB.Reducer] + public static void update_view_pk_player(ReducerContext ctx, ulong id, string name) + { + ctx.Db.view_pk_player.id.Update(new ViewPkPlayer { id = id, name = name }); + } + + [SpacetimeDB.Reducer] + public static void insert_view_pk_membership(ReducerContext ctx, ulong id, ulong player_id) + { + ctx.Db.view_pk_membership.Insert(new ViewPkMembership { id = id, player_id = player_id }); + } + + [SpacetimeDB.Reducer] + public static void insert_view_pk_membership_secondary( + ReducerContext ctx, + ulong id, + ulong player_id + ) + { + ctx.Db.view_pk_membership_secondary.Insert( + new ViewPkMembershipSecondary { id = id, player_id = player_id } + ); + } + + [SpacetimeDB.View(Accessor = "all_view_pk_players", Public = true)] + public static IQuery all_view_pk_players(ViewContext ctx) + { + return ctx.From.view_pk_player(); + } + + [SpacetimeDB.View(Accessor = "sender_view_pk_players_a", Public = true)] + public static IQuery sender_view_pk_players_a(ViewContext ctx) + { + return ctx + .From.view_pk_membership() + .RightSemijoin( + ctx.From.view_pk_player(), + (membership, player) => membership.player_id.Eq(player.id) + ); + } + + [SpacetimeDB.View(Accessor = "sender_view_pk_players_b", Public = true)] + public static IQuery sender_view_pk_players_b(ViewContext ctx) + { + return ctx + .From.view_pk_membership_secondary() + .RightSemijoin( + ctx.From.view_pk_player(), + (membership, player) => membership.player_id.Eq(player.id) + ); + } +} diff --git a/modules/sdk-test-view-pk-cs/README.md b/modules/sdk-test-view-pk-cs/README.md new file mode 100644 index 00000000000..a62bd410bfe --- /dev/null +++ b/modules/sdk-test-view-pk-cs/README.md @@ -0,0 +1,8 @@ +# `sdk-test-view-pk-cs` *C#* test + +See the [sdk-test-view-pk README](../sdk-test-view-pk/README.md) for more details. + +> **WARNING**: This C# source code is manually derived from `../sdk-test-view-pk/src/lib.rs` +> and is supposed to be functionally equivalent. +> Do not add new types or functionality here that are not present in the *Rust* version, +> because they're compared against each other. diff --git a/modules/sdk-test-view-pk-cs/sdk-test-view-pk-cs.csproj b/modules/sdk-test-view-pk-cs/sdk-test-view-pk-cs.csproj new file mode 100644 index 00000000000..d4caede5ffa --- /dev/null +++ b/modules/sdk-test-view-pk-cs/sdk-test-view-pk-cs.csproj @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/sdks/csharp/examples~/regression-tests/client/Program.cs b/sdks/csharp/examples~/regression-tests/client/Program.cs index dda45f13468..c6fc59619c4 100644 --- a/sdks/csharp/examples~/regression-tests/client/Program.cs +++ b/sdks/csharp/examples~/regression-tests/client/Program.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Threading; +using RegressionTests.Shared; using SpacetimeDB; using SpacetimeDB.Types; @@ -19,37 +20,6 @@ const string EXPECTED_TEST_EVENT_NAME = "hello"; const ulong EXPECTED_TEST_EVENT_VALUE = 42; -DbConnection ConnectToDB() -{ - DbConnection? conn = null; - conn = DbConnection - .Builder() - .WithUri(HOST) - .WithDatabaseName(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; var applied = false; SubscriptionHandle? handle = null; @@ -1129,24 +1099,9 @@ ProcedureCallbackResult> result ); } -System.AppDomain.CurrentDomain.UnhandledException += (sender, args) => -{ - Log.Exception($"Unhandled exception: {sender} {args}"); - Environment.Exit(1); -}; -var db = ConnectToDB(); -Log.Info("Starting timer"); +RegressionTestHarness.RegisterUnhandledExceptionExitHandler(); +var db = RegressionTestHarness.ConnectToDatabase(HOST, DBNAME, OnConnected); 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); - } -} +RegressionTestHarness.FrameTickUntilComplete(db, () => applied && waiting == 0, TIMEOUT); Log.Info("Success"); Environment.Exit(0); diff --git a/sdks/csharp/examples~/regression-tests/client/client.csproj b/sdks/csharp/examples~/regression-tests/client/client.csproj index c76a780a74d..540e15ad427 100644 --- a/sdks/csharp/examples~/regression-tests/client/client.csproj +++ b/sdks/csharp/examples~/regression-tests/client/client.csproj @@ -12,4 +12,8 @@ + + + + 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 e6d99f9b15b..52b4bb8deb7 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 2.0.0 (commit 6a6b5a6616f0578aa641bc0689691f953b13feb8). +// This was generated using spacetimedb cli version 2.0.4 (commit dfc726be29516b8cdecc651f5c9705026a624a04). #nullable enable diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/WhereTestQuery.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/WhereTestQuery.g.cs index 8e039b09042..6ebbfe7db97 100644 --- a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/WhereTestQuery.g.cs +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/WhereTestQuery.g.cs @@ -20,6 +20,8 @@ public sealed class WhereTestQueryHandle : RemoteTableHandle row.Id; } public readonly WhereTestQueryHandle WhereTestQuery; @@ -41,9 +43,11 @@ public WhereTestQueryCols(string tableName) public sealed class WhereTestQueryIxCols { + public global::SpacetimeDB.IxCol Id { get; } public WhereTestQueryIxCols(string tableName) { + Id = new global::SpacetimeDB.IxCol(tableName, "id"); } } } diff --git a/sdks/csharp/examples~/regression-tests/procedure-client/Program.cs b/sdks/csharp/examples~/regression-tests/procedure-client/Program.cs index 95e1a0f6508..8997f596354 100644 --- a/sdks/csharp/examples~/regression-tests/procedure-client/Program.cs +++ b/sdks/csharp/examples~/regression-tests/procedure-client/Program.cs @@ -5,38 +5,13 @@ using System.Diagnostics; using System.Runtime.CompilerServices; +using RegressionTests.Shared; using SpacetimeDB; using SpacetimeDB.Types; const string HOST = "http://localhost:3000"; const string DBNAME = "procedure-tests"; -DbConnection ConnectToDB() -{ - DbConnection? conn = null; - conn = DbConnection.Builder() - .WithUri(HOST) - .WithDatabaseName(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; @@ -201,24 +176,9 @@ void OnSubscriptionApplied(SubscriptionEventContext context) }); } -System.AppDomain.CurrentDomain.UnhandledException += (sender, args) => -{ - Log.Exception($"Unhandled exception: {sender} {args}"); - Environment.Exit(1); -}; -var db = ConnectToDB(); -Log.Info("Starting timer"); +RegressionTestHarness.RegisterUnhandledExceptionExitHandler(); +var db = RegressionTestHarness.ConnectToDatabase(HOST, DBNAME, OnConnected); 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); - } -} +RegressionTestHarness.FrameTickUntilComplete(db, () => applied && waiting == 0, TIMEOUT); Log.Info("Success"); Environment.Exit(0); diff --git a/sdks/csharp/examples~/regression-tests/procedure-client/client.csproj b/sdks/csharp/examples~/regression-tests/procedure-client/client.csproj index 590940fbcb0..04759b33920 100644 --- a/sdks/csharp/examples~/regression-tests/procedure-client/client.csproj +++ b/sdks/csharp/examples~/regression-tests/procedure-client/client.csproj @@ -14,4 +14,8 @@ + + + + diff --git a/sdks/csharp/examples~/regression-tests/procedure-client/module_bindings/SpacetimeDBClient.g.cs b/sdks/csharp/examples~/regression-tests/procedure-client/module_bindings/SpacetimeDBClient.g.cs index 59344f7ba69..948b4e405d7 100644 --- a/sdks/csharp/examples~/regression-tests/procedure-client/module_bindings/SpacetimeDBClient.g.cs +++ b/sdks/csharp/examples~/regression-tests/procedure-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 2.0.0 (commit 9e0e81a6aaec6bf3619cfb9f7916743d86ab7ffc). +// This was generated using spacetimedb cli version 2.0.4 (commit dfc726be29516b8cdecc651f5c9705026a624a04). #nullable enable diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/Program.cs b/sdks/csharp/examples~/regression-tests/republishing/client/Program.cs index 52931e7bf7d..e8493cee87f 100644 --- a/sdks/csharp/examples~/regression-tests/republishing/client/Program.cs +++ b/sdks/csharp/examples~/regression-tests/republishing/client/Program.cs @@ -5,38 +5,13 @@ using System.Diagnostics; using System.Runtime.CompilerServices; +using RegressionTests.Shared; 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) - .WithDatabaseName(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; @@ -128,24 +103,9 @@ void OnSubscriptionApplied(SubscriptionEventContext context) 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"); +RegressionTestHarness.RegisterUnhandledExceptionExitHandler(); +var db = RegressionTestHarness.ConnectToDatabase(HOST, DBNAME, OnConnected); 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); - } -} +RegressionTestHarness.FrameTickUntilComplete(db, () => applied && waiting == 0, TIMEOUT); Log.Info("Success"); Environment.Exit(0); diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/client.csproj b/sdks/csharp/examples~/regression-tests/republishing/client/client.csproj index 759ea6f4a26..9c07c1d1c1b 100644 --- a/sdks/csharp/examples~/regression-tests/republishing/client/client.csproj +++ b/sdks/csharp/examples~/regression-tests/republishing/client/client.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/sdks/csharp/examples~/regression-tests/shared/RegressionTestHarness.cs b/sdks/csharp/examples~/regression-tests/shared/RegressionTestHarness.cs new file mode 100644 index 00000000000..b81563ff4a5 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/shared/RegressionTestHarness.cs @@ -0,0 +1,206 @@ +using System.Threading; +using SpacetimeDB; +using SpacetimeDB.Types; + +namespace RegressionTests.Shared; + +internal static class RegressionTestHarness +{ + public static void RegisterUnhandledExceptionExitHandler() + { + AppDomain.CurrentDomain.UnhandledException += (_, eventArgs) => + { + Log.Exception($"Unhandled exception: {eventArgs.ExceptionObject}"); + Environment.Exit(1); + }; + } + + public static void RunNamedTests(string[] args, IReadOnlyDictionary tests) + { + if (args.Length > 1) + { + throw new ArgumentException("Pass zero args (run all) or a single test name."); + } + + if (args.Length == 1) + { + var testName = args[0]; + if (!tests.TryGetValue(testName, out var test)) + { + throw new ArgumentException($"Unknown test: {testName}"); + } + + Log.Info($"Running {testName}"); + test(); + return; + } + + foreach (var (testName, test) in tests) + { + Log.Info($"Running {testName}"); + test(); + } + } + + public static DbConnection ConnectToDatabase( + string host, + string databaseName, + DbConnectionBuilder.ConnectCallback onConnect, + Action? onConnectError = null, + Action? onDisconnect = null + ) + { + return DbConnection + .Builder() + .WithUri(host) + .WithDatabaseName(databaseName) + .OnConnect(onConnect) + .OnConnectError(err => + { + if (onConnectError != null) + { + onConnectError(err); + return; + } + + throw err; + }) + .OnDisconnect((_, err) => + { + if (onDisconnect != null) + { + onDisconnect(err); + return; + } + + if (err != null) + { + throw err; + } + + throw new Exception("Unexpected disconnect"); + }) + .Build(); + } + + public static void RunLiveConnectionTest( + string host, + string databaseName, + string testName, + int timeoutSeconds, + Action> start + ) + { + bool complete = false; + bool disconnectExpected = false; + Exception? failure = null; + + void Pass() => complete = true; + void Fail(Exception error) => failure ??= error; + + var conn = ConnectToDatabase( + host, + databaseName, + (connected, _, _) => + { + try + { + start(connected, Pass, Fail); + } + catch (Exception ex) + { + Fail(ex); + } + }, + onConnectError: Fail, + onDisconnect: err => + { + if (disconnectExpected) + { + return; + } + + if (err != null) + { + Fail(err); + return; + } + + if (!complete) + { + Fail(new Exception($"Unexpected disconnect in {testName}")); + } + } + ); + + FrameTickUntilComplete( + conn, + () => complete || failure != null, + timeoutSeconds, + sleepMilliseconds: 10, + logStart: false + ); + + disconnectExpected = true; + if (conn.IsActive) + { + conn.Disconnect(); + } + + if (failure != null) + { + throw new Exception($"{testName} failed", failure); + } + } + + public static void FrameTickUntilComplete( + DbConnection conn, + Func isComplete, + int timeoutSeconds, + int sleepMilliseconds = 100, + bool logStart = true + ) + { + if (logStart) + { + Log.Info("Starting timer"); + } + + var deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds); + while (!isComplete()) + { + conn.FrameTick(); + Thread.Sleep(sleepMilliseconds); + if (DateTime.UtcNow > deadline) + { + Log.Error($"Timeout, all events should have elapsed in {timeoutSeconds} seconds!"); + Environment.Exit(1); + } + } + } + + public static void Require(bool condition, string message) + { + if (!condition) + { + throw new Exception(message); + } + } + + public static void AssertReducerCommitted(string reducerName, ReducerEventContext ctx) + { + switch (ctx.Event.Status) + { + case Status.Committed: + return; + case Status.Failed(var reason): + throw new Exception($"`{reducerName}` reducer returned error: {reason}"); + case Status.OutOfEnergy(var _): + throw new Exception($"`{reducerName}` reducer ran out of energy"); + default: + throw new Exception( + $"`{reducerName}` reducer returned unexpected status: {ctx.Event.Status}" + ); + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/Program.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/Program.cs new file mode 100644 index 00000000000..7e914265a47 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/Program.cs @@ -0,0 +1,314 @@ +/// View-PK regression tests run with a live server. +/// To run these, start a local SpacetimeDB via `spacetime start`, +/// publish `modules/sdk-test-view-pk-cs`, and then run this client. +using System.Threading; +using RegressionTests.Shared; +using SpacetimeDB; +using SpacetimeDB.Types; + +const string HOST = "http://localhost:3000"; +const string DBNAME = "view-pk-tests"; +const int TIMEOUT_SECONDS = 20; + +long idCounter = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 10; +ulong NextId() => (ulong)Interlocked.Increment(ref idCounter); + +var tests = new Dictionary(StringComparer.Ordinal) +{ + ["view-pk-on-update"] = ExecViewPkOnUpdate, + ["view-pk-join-query-builder"] = ExecViewPkJoinQueryBuilder, + ["view-pk-semijoin-two-sender-views-query-builder"] = + ExecViewPkSemijoinTwoSenderViewsQueryBuilder, +}; + +RegressionTestHarness.RegisterUnhandledExceptionExitHandler(); +RegressionTestHarness.RunNamedTests(args, tests); +Log.Info("Success"); +Environment.Exit(0); + +void RunViewPkTest(string testName, Action> start) +{ + RegressionTestHarness.RunLiveConnectionTest(HOST, DBNAME, testName, TIMEOUT_SECONDS, start); +} + +void RunOrFail(Action work, Action fail) +{ + try + { + work(); + } + catch (Exception ex) + { + fail(ex); + } +} + +void AssertCommittedOrFail(string reducerName, ReducerEventContext ctx, Action fail) +{ + RunOrFail(() => RegressionTestHarness.AssertReducerCommitted(reducerName, ctx), fail); +} + +void ExpectSinglePlayerUpdate( + string testName, + ref bool sawUpdate, + ulong expectedId, + string expectedOldName, + string expectedNewName, + ulong oldId, + string oldName, + ulong newId, + string newName +) +{ + RegressionTestHarness.Require( + !sawUpdate, + $"Expected exactly one OnUpdate callback for {testName}." + ); + RegressionTestHarness.Require(oldId == expectedId, $"Expected oldRow.Id={expectedId}, got {oldId}."); + RegressionTestHarness.Require( + oldName == expectedOldName, + $"Expected oldRow.Name={expectedOldName}, got {oldName}." + ); + RegressionTestHarness.Require(newId == expectedId, $"Expected newRow.Id={expectedId}, got {newId}."); + RegressionTestHarness.Require( + newName == expectedNewName, + $"Expected newRow.Name={expectedNewName}, got {newName}." + ); + sawUpdate = true; +} + +/// Subscribe to a query builder view whose underlying table has a primary key. +/// Ensures the C# SDK emits an `OnUpdate` callback and that the client receives the correct old and new rows. +/// +/// Test: +/// 1. Subscribe to: SELECT * FROM all_view_pk_players +/// 2. Insert row: (id=1, name="before") +/// 3. Update row: (id=1, name="after") +/// +/// Expect: +/// - `OnUpdate` is called for PK=1 +/// - `oldRow` should be the "before" value +/// - `newRow` should be the "after" value +void ExecViewPkOnUpdate() +{ + const string testName = "view-pk-on-update"; + var playerId = NextId(); + const string before = "before"; + const string after = "after"; + + RunViewPkTest(testName, (conn, pass, fail) => + { + bool sawUpdate = false; + + conn.Reducers.OnInsertViewPkPlayer += (ctx, _, _) => + AssertCommittedOrFail("insert_view_pk_player", ctx, fail); + conn.Reducers.OnUpdateViewPkPlayer += (ctx, _, _) => + AssertCommittedOrFail("update_view_pk_player", ctx, fail); + + conn + .SubscriptionBuilder() + .OnApplied(ctx => + RunOrFail( + () => + { + ctx.Db.AllViewPkPlayers.OnUpdate += (_, oldRow, newRow) => + RunOrFail( + () => + { + ExpectSinglePlayerUpdate( + testName, + ref sawUpdate, + playerId, + before, + after, + oldRow.Id, + oldRow.Name, + newRow.Id, + newRow.Name + ); + pass(); + }, + fail + ); + + ctx.Reducers.InsertViewPkPlayer(playerId, before); + ctx.Reducers.UpdateViewPkPlayer(playerId, after); + }, + fail + ) + ) + .OnError((_, err) => fail(err)) + .Subscribe(["SELECT * FROM all_view_pk_players"]); + }); +} + +/// Subscribe to a right semijoin whose rhs is a view with primary key. +/// +/// Ensures: +/// 1. A semijoin subscription involving a view is valid +/// 2. The C# SDK emits an `OnUpdate` callback and that the client receives the correct old and new rows +/// +/// Query: +/// SELECT player.* +/// FROM view_pk_membership membership +/// JOIN all_view_pk_players player ON membership.player_id = player.id +/// +/// Test: +/// 1. Insert player row (id=1, "before"). +/// 2. Insert membership row referencing player_id=1, allowing the semijoin match. +/// 3. Update player row to (id=1, "after"). +/// +/// Expect: +/// - `OnUpdate` is called for player PK=1 +/// - `oldRow` should be the "before" value +/// - `newRow` should be the "after" value +void ExecViewPkJoinQueryBuilder() +{ + const string testName = "view-pk-join-query-builder"; + var playerId = NextId(); + var membershipId = NextId(); + const string before = "before"; + const string after = "after"; + + RunViewPkTest(testName, (conn, pass, fail) => + { + bool sawUpdate = false; + + conn.Reducers.OnInsertViewPkPlayer += (ctx, _, _) => + AssertCommittedOrFail("insert_view_pk_player", ctx, fail); + conn.Reducers.OnInsertViewPkMembership += (ctx, _, _) => + AssertCommittedOrFail("insert_view_pk_membership", ctx, fail); + conn.Reducers.OnUpdateViewPkPlayer += (ctx, _, _) => + AssertCommittedOrFail("update_view_pk_player", ctx, fail); + + conn + .SubscriptionBuilder() + .AddQuery(q => + q.From.ViewPkMembership().RightSemijoin( + q.From.AllViewPkPlayers(), + (membership, player) => membership.PlayerId.Eq(player.Id) + ) + ) + .OnApplied(ctx => + RunOrFail( + () => + { + ctx.Db.AllViewPkPlayers.OnUpdate += (_, oldRow, newRow) => + RunOrFail( + () => + { + ExpectSinglePlayerUpdate( + testName, + ref sawUpdate, + playerId, + before, + after, + oldRow.Id, + oldRow.Name, + newRow.Id, + newRow.Name + ); + pass(); + }, + fail + ); + + ctx.Reducers.InsertViewPkPlayer(playerId, before); + ctx.Reducers.InsertViewPkMembership(membershipId, playerId); + ctx.Reducers.UpdateViewPkPlayer(playerId, after); + }, + fail + ) + ) + .OnError((_, err) => fail(err)) + .Subscribe(); + }); +} + +/// Subscribe to a semijoin between two views with primary keys. +/// +/// Ensures: +/// 1. A semijoin subscription involving a view is valid +/// 2. The C# SDK emits an `OnUpdate` callback and that the client receives the correct old and new rows +/// +/// Query: +/// SELECT b.* +/// FROM sender_view_pk_players_a a +/// JOIN sender_view_pk_players_b b ON a.id = b.id +/// +/// Test: +/// 1. Insert player row (id=1, "before"). +/// 2. Insert membership for sender view A. +/// 3. Insert membership for sender view B. +/// 4. Update player row to (id=1, "after"). +/// +/// Expect: +/// - `OnUpdate` is called for player PK=1 +/// - `oldRow` should be the "before" value +/// - `newRow` should be the "after" value +void ExecViewPkSemijoinTwoSenderViewsQueryBuilder() +{ + const string testName = "view-pk-semijoin-two-sender-views-query-builder"; + var playerId = NextId(); + var membershipAId = NextId(); + var membershipBId = NextId(); + const string before = "before"; + const string after = "after"; + + RunViewPkTest(testName, (conn, pass, fail) => + { + bool sawUpdate = false; + + conn.Reducers.OnInsertViewPkPlayer += (ctx, _, _) => + AssertCommittedOrFail("insert_view_pk_player", ctx, fail); + conn.Reducers.OnInsertViewPkMembership += (ctx, _, _) => + AssertCommittedOrFail("insert_view_pk_membership", ctx, fail); + conn.Reducers.OnInsertViewPkMembershipSecondary += (ctx, _, _) => + AssertCommittedOrFail("insert_view_pk_membership_secondary", ctx, fail); + conn.Reducers.OnUpdateViewPkPlayer += (ctx, _, _) => + AssertCommittedOrFail("update_view_pk_player", ctx, fail); + + conn + .SubscriptionBuilder() + .AddQuery(q => + q.From.SenderViewPkPlayersA().RightSemijoin( + q.From.SenderViewPkPlayersB(), + (lhsView, rhsView) => lhsView.Id.Eq(rhsView.Id) + ) + ) + .OnApplied(ctx => + RunOrFail( + () => + { + ctx.Db.SenderViewPkPlayersB.OnUpdate += (_, oldRow, newRow) => + RunOrFail( + () => + { + ExpectSinglePlayerUpdate( + testName, + ref sawUpdate, + playerId, + before, + after, + oldRow.Id, + oldRow.Name, + newRow.Id, + newRow.Name + ); + pass(); + }, + fail + ); + + ctx.Reducers.InsertViewPkPlayer(playerId, before); + ctx.Reducers.InsertViewPkMembership(membershipAId, playerId); + ctx.Reducers.InsertViewPkMembershipSecondary(membershipBId, playerId); + ctx.Reducers.UpdateViewPkPlayer(playerId, after); + }, + fail + ) + ) + .OnError((_, err) => fail(err)) + .Subscribe(); + }); +} diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/client.csproj b/sdks/csharp/examples~/regression-tests/view-pk-client/client.csproj new file mode 100644 index 00000000000..bf7c38298e1 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/client.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + true + + + + + + + + + + + diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Reducers/InsertViewPkMembership.g.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Reducers/InsertViewPkMembership.g.cs new file mode 100644 index 00000000000..3b09c569584 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Reducers/InsertViewPkMembership.g.cs @@ -0,0 +1,73 @@ +// 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 InsertViewPkMembershipHandler(ReducerEventContext ctx, ulong id, ulong playerId); + public event InsertViewPkMembershipHandler? OnInsertViewPkMembership; + + public void InsertViewPkMembership(ulong id, ulong playerId) + { + conn.InternalCallReducer(new Reducer.InsertViewPkMembership(id, playerId)); + } + + public bool InvokeInsertViewPkMembership(ReducerEventContext ctx, Reducer.InsertViewPkMembership args) + { + if (OnInsertViewPkMembership == 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; + } + OnInsertViewPkMembership( + ctx, + args.Id, + args.PlayerId + ); + return true; + } + } + + public abstract partial class Reducer + { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class InsertViewPkMembership : Reducer, IReducerArgs + { + [DataMember(Name = "id")] + public ulong Id; + [DataMember(Name = "player_id")] + public ulong PlayerId; + + public InsertViewPkMembership( + ulong Id, + ulong PlayerId + ) + { + this.Id = Id; + this.PlayerId = PlayerId; + } + + public InsertViewPkMembership() + { + } + + string IReducerArgs.ReducerName => "insert_view_pk_membership"; + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Reducers/InsertViewPkMembershipSecondary.g.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Reducers/InsertViewPkMembershipSecondary.g.cs new file mode 100644 index 00000000000..4742217d359 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Reducers/InsertViewPkMembershipSecondary.g.cs @@ -0,0 +1,73 @@ +// 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 InsertViewPkMembershipSecondaryHandler(ReducerEventContext ctx, ulong id, ulong playerId); + public event InsertViewPkMembershipSecondaryHandler? OnInsertViewPkMembershipSecondary; + + public void InsertViewPkMembershipSecondary(ulong id, ulong playerId) + { + conn.InternalCallReducer(new Reducer.InsertViewPkMembershipSecondary(id, playerId)); + } + + public bool InvokeInsertViewPkMembershipSecondary(ReducerEventContext ctx, Reducer.InsertViewPkMembershipSecondary args) + { + if (OnInsertViewPkMembershipSecondary == 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; + } + OnInsertViewPkMembershipSecondary( + ctx, + args.Id, + args.PlayerId + ); + return true; + } + } + + public abstract partial class Reducer + { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class InsertViewPkMembershipSecondary : Reducer, IReducerArgs + { + [DataMember(Name = "id")] + public ulong Id; + [DataMember(Name = "player_id")] + public ulong PlayerId; + + public InsertViewPkMembershipSecondary( + ulong Id, + ulong PlayerId + ) + { + this.Id = Id; + this.PlayerId = PlayerId; + } + + public InsertViewPkMembershipSecondary() + { + } + + string IReducerArgs.ReducerName => "insert_view_pk_membership_secondary"; + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Reducers/InsertViewPkPlayer.g.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Reducers/InsertViewPkPlayer.g.cs new file mode 100644 index 00000000000..75146b10f70 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Reducers/InsertViewPkPlayer.g.cs @@ -0,0 +1,74 @@ +// 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 InsertViewPkPlayerHandler(ReducerEventContext ctx, ulong id, string name); + public event InsertViewPkPlayerHandler? OnInsertViewPkPlayer; + + public void InsertViewPkPlayer(ulong id, string name) + { + conn.InternalCallReducer(new Reducer.InsertViewPkPlayer(id, name)); + } + + public bool InvokeInsertViewPkPlayer(ReducerEventContext ctx, Reducer.InsertViewPkPlayer args) + { + if (OnInsertViewPkPlayer == 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; + } + OnInsertViewPkPlayer( + ctx, + args.Id, + args.Name + ); + return true; + } + } + + public abstract partial class Reducer + { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class InsertViewPkPlayer : Reducer, IReducerArgs + { + [DataMember(Name = "id")] + public ulong Id; + [DataMember(Name = "name")] + public string Name; + + public InsertViewPkPlayer( + ulong Id, + string Name + ) + { + this.Id = Id; + this.Name = Name; + } + + public InsertViewPkPlayer() + { + this.Name = ""; + } + + string IReducerArgs.ReducerName => "insert_view_pk_player"; + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Reducers/UpdateViewPkPlayer.g.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Reducers/UpdateViewPkPlayer.g.cs new file mode 100644 index 00000000000..1649aed0401 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Reducers/UpdateViewPkPlayer.g.cs @@ -0,0 +1,74 @@ +// 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 UpdateViewPkPlayerHandler(ReducerEventContext ctx, ulong id, string name); + public event UpdateViewPkPlayerHandler? OnUpdateViewPkPlayer; + + public void UpdateViewPkPlayer(ulong id, string name) + { + conn.InternalCallReducer(new Reducer.UpdateViewPkPlayer(id, name)); + } + + public bool InvokeUpdateViewPkPlayer(ReducerEventContext ctx, Reducer.UpdateViewPkPlayer args) + { + if (OnUpdateViewPkPlayer == 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; + } + OnUpdateViewPkPlayer( + ctx, + args.Id, + args.Name + ); + return true; + } + } + + public abstract partial class Reducer + { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class UpdateViewPkPlayer : Reducer, IReducerArgs + { + [DataMember(Name = "id")] + public ulong Id; + [DataMember(Name = "name")] + public string Name; + + public UpdateViewPkPlayer( + ulong Id, + string Name + ) + { + this.Id = Id; + this.Name = Name; + } + + public UpdateViewPkPlayer() + { + this.Name = ""; + } + + string IReducerArgs.ReducerName => "update_view_pk_player"; + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/SpacetimeDBClient.g.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/SpacetimeDBClient.g.cs new file mode 100644 index 00000000000..a43949b6a86 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/SpacetimeDBClient.g.cs @@ -0,0 +1,646 @@ +// 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 2.0.4 (commit dfc726be29516b8cdecc651f5c9705026a624a04). + +#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) : base(conn) { } + internal event Action? InternalOnUnhandledReducerError; + } + + public sealed partial class RemoteProcedures : RemoteBase + { + internal RemoteProcedures(DbConnection conn) : base(conn) { } + } + + public sealed partial class RemoteTables : RemoteTablesBase + { + public RemoteTables(DbConnection conn) + { + AddTable(AllViewPkPlayers = new(conn)); + AddTable(SenderViewPkPlayersA = new(conn)); + AddTable(SenderViewPkPlayersB = new(conn)); + AddTable(ViewPkMembership = new(conn)); + AddTable(ViewPkMembershipSecondary = new(conn)); + AddTable(ViewPkPlayer = new(conn)); + } + } + + + 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 procedures defined by the module. + /// + /// The returned RemoteProcedures will have a method to invoke each procedure defined by the module, + /// with a callback for when the procedure completes and returns a value. + /// + public RemoteProcedures Procedures => conn.Procedures; + /// + /// 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 procedures defined by the module. + /// + /// The returned RemoteProcedures will have a method to invoke each procedure defined by the module, + /// with a callback for when the procedure completes and returns a value. + /// + public RemoteProcedures Procedures => conn.Procedures; + /// + /// 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 procedures defined by the module. + /// + /// The returned RemoteProcedures will have a method to invoke each procedure defined by the module, + /// with a callback for when the procedure completes and returns a value. + /// + public RemoteProcedures Procedures => conn.Procedures; + /// + /// 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 procedures defined by the module. + /// + /// The returned RemoteProcedures will have a method to invoke each procedure defined by the module, + /// with a callback for when the procedure completes and returns a value. + /// + public RemoteProcedures Procedures => conn.Procedures; + /// + /// 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; + } + } + + public sealed class ProcedureEventContext : IProcedureEventContext, IRemoteDbContext + { + private readonly DbConnection conn; + /// + /// The procedure event that caused this callback to run. + /// + public readonly ProcedureEvent 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 procedures defined by the module. + /// + /// The returned RemoteProcedures will have a method to invoke each procedure defined by the module, + /// with a callback for when the procedure completes and returns a value. + /// + public RemoteProcedures Procedures => conn.Procedures; + /// + /// 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 ProcedureEventContext(DbConnection conn, ProcedureEvent Event) + { + this.conn = conn; + this.Event = Event; + } + } + + /// + /// 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; + } + + /// + /// Add a typed query to this subscription. + /// + /// This is the entry point for building subscriptions without writing SQL by hand. + /// Once a typed query is added, only typed queries may follow (SQL and typed queries cannot be mixed). + /// + public TypedSubscriptionBuilder AddQuery( + Func> build + ) + { + var typed = new TypedSubscriptionBuilder(conn, Applied, Error); + return typed.AddQuery(build); + } + + /// + /// 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 SubscriptionHandle SubscribeToAllTables() => + new(conn, Applied, Error, QueryBuilder.AllTablesSqlQueries()); + } + + public sealed class SubscriptionHandle : SubscriptionHandleBase + { + /// + /// Internal API. Construct SubscriptionHandles using conn.SubscriptionBuilder. + /// + public SubscriptionHandle( + IDbConnection conn, + Action? onApplied, + Action? onError, + string[] querySqls + ) : base(conn, onApplied, onError, querySqls) + { } + } + + public sealed class QueryBuilder + { + public From From { get; } = new(); + + internal static string[] AllTablesSqlQueries() => new string[] + { + new QueryBuilder().From.AllViewPkPlayers().ToSql(), + new QueryBuilder().From.SenderViewPkPlayersA().ToSql(), + new QueryBuilder().From.SenderViewPkPlayersB().ToSql(), + new QueryBuilder().From.ViewPkMembership().ToSql(), + new QueryBuilder().From.ViewPkMembershipSecondary().ToSql(), + new QueryBuilder().From.ViewPkPlayer().ToSql(), + } + ; + } + + public sealed class From + { + public global::SpacetimeDB.Table AllViewPkPlayers() => new("all_view_pk_players", new AllViewPkPlayersCols("all_view_pk_players"), new AllViewPkPlayersIxCols("all_view_pk_players")); + public global::SpacetimeDB.Table SenderViewPkPlayersA() => new("sender_view_pk_players_a", new SenderViewPkPlayersACols("sender_view_pk_players_a"), new SenderViewPkPlayersAIxCols("sender_view_pk_players_a")); + public global::SpacetimeDB.Table SenderViewPkPlayersB() => new("sender_view_pk_players_b", new SenderViewPkPlayersBCols("sender_view_pk_players_b"), new SenderViewPkPlayersBIxCols("sender_view_pk_players_b")); + public global::SpacetimeDB.Table ViewPkMembership() => new("view_pk_membership", new ViewPkMembershipCols("view_pk_membership"), new ViewPkMembershipIxCols("view_pk_membership")); + public global::SpacetimeDB.Table ViewPkMembershipSecondary() => new("view_pk_membership_secondary", new ViewPkMembershipSecondaryCols("view_pk_membership_secondary"), new ViewPkMembershipSecondaryIxCols("view_pk_membership_secondary")); + public global::SpacetimeDB.Table ViewPkPlayer() => new("view_pk_player", new ViewPkPlayerCols("view_pk_player"), new ViewPkPlayerIxCols("view_pk_player")); + } + + public sealed class TypedSubscriptionBuilder + { + private readonly IDbConnection conn; + private Action? Applied; + private Action? Error; + private readonly List querySqls = new(); + + internal TypedSubscriptionBuilder(IDbConnection conn, Action? applied, Action? error) + { + this.conn = conn; + Applied = applied; + Error = error; + } + + public TypedSubscriptionBuilder OnApplied(Action callback) + { + Applied += callback; + return this; + } + + public TypedSubscriptionBuilder OnError(Action callback) + { + Error += callback; + return this; + } + + public TypedSubscriptionBuilder AddQuery(Func> build) + { + var qb = new QueryBuilder(); + querySqls.Add(build(qb).ToSql()); + return this; + } + + public SubscriptionHandle Subscribe() => new(conn, Applied, Error, querySqls.ToArray()); + } + + public abstract partial class Reducer + { + private Reducer() { } + } + + public abstract partial class Procedure + { + private Procedure() { } + } + + public sealed class DbConnection : DbConnectionBase + { + public override RemoteTables Db { get; } + public readonly RemoteReducers Reducers; + public readonly RemoteProcedures Procedures; + + public DbConnection() + { + Db = new(this); + Reducers = new(this); + Procedures = new(this); + } + + 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 IProcedureEventContext ToProcedureEventContext(ProcedureEvent procedureEvent) => + new ProcedureEventContext(this, procedureEvent); + + protected override bool Dispatch(IReducerEventContext context, Reducer reducer) + { + var eventContext = (ReducerEventContext)context; + return reducer switch + { + Reducer.InsertViewPkMembership args => Reducers.InvokeInsertViewPkMembership(eventContext, args), + Reducer.InsertViewPkMembershipSecondary args => Reducers.InvokeInsertViewPkMembershipSecondary(eventContext, args), + Reducer.InsertViewPkPlayer args => Reducers.InvokeInsertViewPkPlayer(eventContext, args), + Reducer.UpdateViewPkPlayer args => Reducers.InvokeUpdateViewPkPlayer(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/view-pk-client/module_bindings/Tables/AllViewPkPlayers.g.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/AllViewPkPlayers.g.cs new file mode 100644 index 00000000000..98b348c7f09 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/AllViewPkPlayers.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 AllViewPkPlayersHandle : RemoteTableHandle + { + protected override string RemoteTableName => "all_view_pk_players"; + + internal AllViewPkPlayersHandle(DbConnection conn) : base(conn) + { + } + + protected override object GetPrimaryKey(ViewPkPlayer row) => row.Id; + } + + public readonly AllViewPkPlayersHandle AllViewPkPlayers; + } + + public sealed class AllViewPkPlayersCols + { + public global::SpacetimeDB.Col Id { get; } + public global::SpacetimeDB.Col Name { get; } + + public AllViewPkPlayersCols(string tableName) + { + Id = new global::SpacetimeDB.Col(tableName, "id"); + Name = new global::SpacetimeDB.Col(tableName, "name"); + } + } + + public sealed class AllViewPkPlayersIxCols + { + public global::SpacetimeDB.IxCol Id { get; } + + public AllViewPkPlayersIxCols(string tableName) + { + Id = new global::SpacetimeDB.IxCol(tableName, "id"); + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/SenderViewPkPlayersA.g.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/SenderViewPkPlayersA.g.cs new file mode 100644 index 00000000000..bc23d9e62ef --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/SenderViewPkPlayersA.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 SenderViewPkPlayersAHandle : RemoteTableHandle + { + protected override string RemoteTableName => "sender_view_pk_players_a"; + + internal SenderViewPkPlayersAHandle(DbConnection conn) : base(conn) + { + } + + protected override object GetPrimaryKey(ViewPkPlayer row) => row.Id; + } + + public readonly SenderViewPkPlayersAHandle SenderViewPkPlayersA; + } + + public sealed class SenderViewPkPlayersACols + { + public global::SpacetimeDB.Col Id { get; } + public global::SpacetimeDB.Col Name { get; } + + public SenderViewPkPlayersACols(string tableName) + { + Id = new global::SpacetimeDB.Col(tableName, "id"); + Name = new global::SpacetimeDB.Col(tableName, "name"); + } + } + + public sealed class SenderViewPkPlayersAIxCols + { + public global::SpacetimeDB.IxCol Id { get; } + + public SenderViewPkPlayersAIxCols(string tableName) + { + Id = new global::SpacetimeDB.IxCol(tableName, "id"); + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/SenderViewPkPlayersB.g.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/SenderViewPkPlayersB.g.cs new file mode 100644 index 00000000000..214837f8276 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/SenderViewPkPlayersB.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 SenderViewPkPlayersBHandle : RemoteTableHandle + { + protected override string RemoteTableName => "sender_view_pk_players_b"; + + internal SenderViewPkPlayersBHandle(DbConnection conn) : base(conn) + { + } + + protected override object GetPrimaryKey(ViewPkPlayer row) => row.Id; + } + + public readonly SenderViewPkPlayersBHandle SenderViewPkPlayersB; + } + + public sealed class SenderViewPkPlayersBCols + { + public global::SpacetimeDB.Col Id { get; } + public global::SpacetimeDB.Col Name { get; } + + public SenderViewPkPlayersBCols(string tableName) + { + Id = new global::SpacetimeDB.Col(tableName, "id"); + Name = new global::SpacetimeDB.Col(tableName, "name"); + } + } + + public sealed class SenderViewPkPlayersBIxCols + { + public global::SpacetimeDB.IxCol Id { get; } + + public SenderViewPkPlayersBIxCols(string tableName) + { + Id = new global::SpacetimeDB.IxCol(tableName, "id"); + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/ViewPkMembership.g.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/ViewPkMembership.g.cs new file mode 100644 index 00000000000..8c0339e7b86 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/ViewPkMembership.g.cs @@ -0,0 +1,73 @@ +// 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 ViewPkMembershipHandle : RemoteTableHandle + { + protected override string RemoteTableName => "view_pk_membership"; + + public sealed class IdUniqueIndex : UniqueIndexBase + { + protected override ulong GetKey(ViewPkMembership row) => row.Id; + + public IdUniqueIndex(ViewPkMembershipHandle table) : base(table) { } + } + + public readonly IdUniqueIndex Id; + + public sealed class PlayerIdIndex : BTreeIndexBase + { + protected override ulong GetKey(ViewPkMembership row) => row.PlayerId; + + public PlayerIdIndex(ViewPkMembershipHandle table) : base(table) { } + } + + public readonly PlayerIdIndex PlayerId; + + internal ViewPkMembershipHandle(DbConnection conn) : base(conn) + { + Id = new(this); + PlayerId = new(this); + } + + protected override object GetPrimaryKey(ViewPkMembership row) => row.Id; + } + + public readonly ViewPkMembershipHandle ViewPkMembership; + } + + public sealed class ViewPkMembershipCols + { + public global::SpacetimeDB.Col Id { get; } + public global::SpacetimeDB.Col PlayerId { get; } + + public ViewPkMembershipCols(string tableName) + { + Id = new global::SpacetimeDB.Col(tableName, "id"); + PlayerId = new global::SpacetimeDB.Col(tableName, "player_id"); + } + } + + public sealed class ViewPkMembershipIxCols + { + public global::SpacetimeDB.IxCol Id { get; } + public global::SpacetimeDB.IxCol PlayerId { get; } + + public ViewPkMembershipIxCols(string tableName) + { + Id = new global::SpacetimeDB.IxCol(tableName, "id"); + PlayerId = new global::SpacetimeDB.IxCol(tableName, "player_id"); + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/ViewPkMembershipSecondary.g.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/ViewPkMembershipSecondary.g.cs new file mode 100644 index 00000000000..ec4bf1ff25a --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/ViewPkMembershipSecondary.g.cs @@ -0,0 +1,73 @@ +// 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 ViewPkMembershipSecondaryHandle : RemoteTableHandle + { + protected override string RemoteTableName => "view_pk_membership_secondary"; + + public sealed class IdUniqueIndex : UniqueIndexBase + { + protected override ulong GetKey(ViewPkMembershipSecondary row) => row.Id; + + public IdUniqueIndex(ViewPkMembershipSecondaryHandle table) : base(table) { } + } + + public readonly IdUniqueIndex Id; + + public sealed class PlayerIdIndex : BTreeIndexBase + { + protected override ulong GetKey(ViewPkMembershipSecondary row) => row.PlayerId; + + public PlayerIdIndex(ViewPkMembershipSecondaryHandle table) : base(table) { } + } + + public readonly PlayerIdIndex PlayerId; + + internal ViewPkMembershipSecondaryHandle(DbConnection conn) : base(conn) + { + Id = new(this); + PlayerId = new(this); + } + + protected override object GetPrimaryKey(ViewPkMembershipSecondary row) => row.Id; + } + + public readonly ViewPkMembershipSecondaryHandle ViewPkMembershipSecondary; + } + + public sealed class ViewPkMembershipSecondaryCols + { + public global::SpacetimeDB.Col Id { get; } + public global::SpacetimeDB.Col PlayerId { get; } + + public ViewPkMembershipSecondaryCols(string tableName) + { + Id = new global::SpacetimeDB.Col(tableName, "id"); + PlayerId = new global::SpacetimeDB.Col(tableName, "player_id"); + } + } + + public sealed class ViewPkMembershipSecondaryIxCols + { + public global::SpacetimeDB.IxCol Id { get; } + public global::SpacetimeDB.IxCol PlayerId { get; } + + public ViewPkMembershipSecondaryIxCols(string tableName) + { + Id = new global::SpacetimeDB.IxCol(tableName, "id"); + PlayerId = new global::SpacetimeDB.IxCol(tableName, "player_id"); + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/ViewPkPlayer.g.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/ViewPkPlayer.g.cs new file mode 100644 index 00000000000..f420e3e2586 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Tables/ViewPkPlayer.g.cs @@ -0,0 +1,61 @@ +// 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 ViewPkPlayerHandle : RemoteTableHandle + { + protected override string RemoteTableName => "view_pk_player"; + + public sealed class IdUniqueIndex : UniqueIndexBase + { + protected override ulong GetKey(ViewPkPlayer row) => row.Id; + + public IdUniqueIndex(ViewPkPlayerHandle table) : base(table) { } + } + + public readonly IdUniqueIndex Id; + + internal ViewPkPlayerHandle(DbConnection conn) : base(conn) + { + Id = new(this); + } + + protected override object GetPrimaryKey(ViewPkPlayer row) => row.Id; + } + + public readonly ViewPkPlayerHandle ViewPkPlayer; + } + + public sealed class ViewPkPlayerCols + { + public global::SpacetimeDB.Col Id { get; } + public global::SpacetimeDB.Col Name { get; } + + public ViewPkPlayerCols(string tableName) + { + Id = new global::SpacetimeDB.Col(tableName, "id"); + Name = new global::SpacetimeDB.Col(tableName, "name"); + } + } + + public sealed class ViewPkPlayerIxCols + { + public global::SpacetimeDB.IxCol Id { get; } + + public ViewPkPlayerIxCols(string tableName) + { + Id = new global::SpacetimeDB.IxCol(tableName, "id"); + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Types/ViewPkMembership.g.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Types/ViewPkMembership.g.cs new file mode 100644 index 00000000000..330ba5af904 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Types/ViewPkMembership.g.cs @@ -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 ViewPkMembership + { + [DataMember(Name = "id")] + public ulong Id; + [DataMember(Name = "player_id")] + public ulong PlayerId; + + public ViewPkMembership( + ulong Id, + ulong PlayerId + ) + { + this.Id = Id; + this.PlayerId = PlayerId; + } + + public ViewPkMembership() + { + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Types/ViewPkMembershipSecondary.g.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Types/ViewPkMembershipSecondary.g.cs new file mode 100644 index 00000000000..aec64d3466b --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Types/ViewPkMembershipSecondary.g.cs @@ -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 ViewPkMembershipSecondary + { + [DataMember(Name = "id")] + public ulong Id; + [DataMember(Name = "player_id")] + public ulong PlayerId; + + public ViewPkMembershipSecondary( + ulong Id, + ulong PlayerId + ) + { + this.Id = Id; + this.PlayerId = PlayerId; + } + + public ViewPkMembershipSecondary() + { + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Types/ViewPkPlayer.g.cs b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Types/ViewPkPlayer.g.cs new file mode 100644 index 00000000000..b3b1da245ca --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/view-pk-client/module_bindings/Types/ViewPkPlayer.g.cs @@ -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 ViewPkPlayer + { + [DataMember(Name = "id")] + public ulong Id; + [DataMember(Name = "name")] + public string Name; + + public ViewPkPlayer( + ulong Id, + string Name + ) + { + this.Id = Id; + this.Name = Name; + } + + public ViewPkPlayer() + { + this.Name = ""; + } + } +} diff --git a/sdks/csharp/tools~/gen-regression-tests.sh b/sdks/csharp/tools~/gen-regression-tests.sh index 8936971cd5a..451a2131a60 100755 --- a/sdks/csharp/tools~/gen-regression-tests.sh +++ b/sdks/csharp/tools~/gen-regression-tests.sh @@ -10,3 +10,4 @@ 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" --module-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" --module-path "$SDK_PATH/examples~/regression-tests/republishing/server-republish" cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- generate -y -l csharp -o "$SDK_PATH/examples~/regression-tests/procedure-client/module_bindings" --module-path "$STDB_PATH/modules/sdk-test-procedure" +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- generate -y -l csharp -o "$SDK_PATH/examples~/regression-tests/view-pk-client/module_bindings" --module-path "$STDB_PATH/modules/sdk-test-view-pk-cs" diff --git a/sdks/csharp/tools~/run-regression-tests.sh b/sdks/csharp/tools~/run-regression-tests.sh index fdd7733bf65..8ce171384cb 100644 --- a/sdks/csharp/tools~/run-regression-tests.sh +++ b/sdks/csharp/tools~/run-regression-tests.sh @@ -23,14 +23,18 @@ cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" call --server local cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish --server local -p "$SDK_PATH/examples~/regression-tests/republishing/server-republish" --break-clients republish-test cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" call --server local republish-test insert 2 -echo "Cleanup obj~ folders generated in $SDK_PATH/examples~/regression-tests/procedure-client" +echo "Cleanup obj~ folders generated in $SDK_PATH/examples~/regression-tests/procedure-client and $SDK_PATH/examples~/regression-tests/view-pk-client" # There is a bug in the code generator that creates obj~ folders in the output directory using a Rust project. rm -rf "$SDK_PATH/examples~/regression-tests/procedure-client"/*/obj~ rm -rf "$SDK_PATH/examples~/regression-tests/procedure-client/module_bindings"/*/obj~ +rm -rf "$SDK_PATH/examples~/regression-tests/view-pk-client"/*/obj~ +rm -rf "$SDK_PATH/examples~/regression-tests/view-pk-client/module_bindings"/*/obj~ # Publish module for procedure tests cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server local -p "$STDB_PATH/modules/sdk-test-procedure" procedure-tests +# Publish module for view-pk tests +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server local -p "$STDB_PATH/modules/sdk-test-view-pk-cs" view-pk-tests # Run client for btree test cd "$SDK_PATH/examples~/regression-tests/client" && dotnet run -c Debug @@ -40,3 +44,6 @@ cd "$SDK_PATH/examples~/regression-tests/republishing/client" && dotnet run -c D # Run client for procedure test cd "$SDK_PATH/examples~/regression-tests/procedure-client" && dotnet run -c Debug + +# Run client for view-pk tests +cd "$SDK_PATH/examples~/regression-tests/view-pk-client" && dotnet run -c Debug diff --git a/sdks/rust/tests/test.rs b/sdks/rust/tests/test.rs index d1b417ac360..a7917eaad05 100644 --- a/sdks/rust/tests/test.rs +++ b/sdks/rust/tests/test.rs @@ -529,3 +529,4 @@ macro_rules! view_pk_tests { view_pk_tests!(rust_view_pk, ""); view_pk_tests!(typescript_view_pk, "-ts"); +view_pk_tests!(csharp_view_pk, "-cs");