Files
Ryan 4f0a21f948 C# client typed query builder (#3982)
# Description of Changes
This PR implements the C# client-side typed query builder, as assigned
in https://github.com/clockworklabs/SpacetimeDB/issues/3759.

Key pieces:
* Added a small C# runtime query-builder surface in the client SDK
(`sdks/csharp/src/QueryBuilder.cs`):
  * `Query` (wraps the generated SQL string)
* `Table<TRow, TCols, TIxCols>` (entry point for `All()` / `Where(...)`)
* `Col<TRow, TValue>` and `IxCol<TRow, TValue>` (typed column
references)
  * `BoolExpr` (typed boolean expression composition)
  * SQL identifier quoting + literal formatting helpers (`SqlFormat`)
  * `Join(...)` with `WhereLeft(...)` / `WhereRight(...)`
* `LeftSemijoin(...)` / `RightSemijoin(...)` with `Where(...)` chaining
* Extended C# client bindings codegen (`crates/codegen/src/csharp.rs`)
to generate:
* Per-table/view `*Cols` and `*IxCols` helper classes used by the typed
query builder.
* A generated per-module `QueryBuilder` with a `From` accessor for each
table/view, producing `Table<...>` values.
* A generated `TypedSubscriptionBuilder` which collects
`Query<TRow>.Sql` values and calls the existing subscription API.
* An `AddQuery(Func<QueryBuilder, Query> build)` entry point off
`SubscriptionBuilder`, mirroring the proposal’s Rust API.
* Fixed a codegen naming collision found during regression testing:
* `*Cols`/`*IxCols` helpers are now named after the table/view accessor
name (PascalCase) instead of the row type, since multiple tables/views
can share the same row type (e.g. alias tables / views returning an
existing product type).
* Kept `Cols`/`IxCols` off the public surface:
* `Table.Cols` and `Table.IxCols` are internal, so consumers only access
columns via the `Where(...)`/join predicate lambdas.

C# usage examples (mirroring the proposal’s Rust examples)
1) Typed subscription flow (no raw SQL)
```csharp
void Subscribe(SpacetimeDB.Types.DbConnection conn)
{
conn.SubscriptionBuilder()
    .OnApplied(ctx => { /* ... */ })
    .OnError((ctx, err) => { /* ... */ })
    .AddQuery(qb => qb.From.Users().Build())
    .AddQuery(qb => qb.From.Players().Build())
    .Subscribe();
}
```
2) Typed `WHERE` filters and boolean composition
```csharp
conn.SubscriptionBuilder()
    .OnApplied(ctx => { /* ... */ })
    .OnError((ctx, err) => { /* ... */ })
    .AddQuery(qb => qb.From.Players().Where(p => p.Name.Eq("alice").And(p.IsOnline.Eq(true))).Build())
    .Subscribe();
```
3) “Admin can see all, otherwise only self” (proposal’s “player” view
logic, but client-side)
```csharp
Identity self = /* ... */;

conn.SubscriptionBuilder()
    .AddQuery(qb =>
        qb.From.Players().Where(p =>
            p.Identity.Eq(self)
        )
    )
    .Subscribe();
```
4) Index-column access for query construction (IxCols)
```csharp
conn.SubscriptionBuilder()
    .AddQuery(qb =>
        qb.From.Players().Where(
            qb.From.Players().IxCols.Identity.Eq(self)
        )
    )
    .Subscribe();
```
# API and ABI breaking changes
None.
* Additive client SDK runtime types.
* Additive client bindings codegen output.
* No wire-format changes.
# Expected complexity level and risk
2 - Low to moderate
* Mostly additive code + codegen.
* The main risk is correctness/compat of generated SQL strings and
name/casing conventions across languages; this is mitigated by targeted
unit tests + full C# regression test runs.
# Testing
- [X] Ran run-regression-tests.sh successfully after regenerating C#
bindings.
- [X] Ran C# unit tests using `dotnet test
sdks/csharp/tests~/tests.csproj -c Release`
- [X] Added a new unit test suite
(`sdks/csharp/tests~/QueryBuilderTests.cs`) validating:
  * Identifier quoting / escaping
* Literal formatting (including `Identity`/`ConnectionId`/`Uuid` hex
literals; `U128` integer literal)
  * null + enum unsupported behavior throws
  * Boolean expression parenthesization (`And`/`Or`/`Not`)
  * `Where(...)` overloads including `IxCols`-based predicates
  * left/right semijoin SQL formatting and predicate chaining
2026-01-28 02:12:59 +00:00

699 lines
28 KiB
C#
Generated

// 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.2 (commit 97aa69de8942102a6ea0b50dfadea3cd15e44f50).
#nullable enable
using System;
using SpacetimeDB.ClientApi;
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace SpacetimeDB.Types
{
public sealed partial class RemoteReducers : RemoteBase
{
internal RemoteReducers(DbConnection conn, SetReducerFlags flags) : base(conn) => SetCallReducerFlags = flags;
internal readonly SetReducerFlags SetCallReducerFlags;
internal event Action<ReducerEventContext, Exception>? InternalOnUnhandledReducerError;
}
public sealed partial class RemoteProcedures : RemoteBase
{
internal RemoteProcedures(DbConnection conn) : base(conn) { }
}
public sealed partial class RemoteTables : RemoteTablesBase
{
public RemoteTables(DbConnection conn)
{
AddTable(Message = new(conn));
AddTable(User = new(conn));
}
}
public sealed partial class SetReducerFlags { }
public interface IRemoteDbContext : IDbContext<RemoteTables, RemoteReducers, SetReducerFlags, SubscriptionBuilder, RemoteProcedures>
{
public event Action<ReducerEventContext, Exception>? OnUnhandledReducerError;
}
public sealed class EventContext : IEventContext, IRemoteDbContext
{
private readonly DbConnection conn;
/// <summary>
/// The event that caused this callback to run.
/// </summary>
public readonly Event<Reducer> Event;
/// <summary>
/// Access to tables in the client cache, which stores a read-only replica of the remote database state.
///
/// The returned <c>DbView</c> will have a method to access each table defined by the module.
/// </summary>
public RemoteTables Db => conn.Db;
/// <summary>
/// Access to reducers defined by the module.
///
/// The returned <c>RemoteReducers</c> will have a method to invoke each reducer defined by the module,
/// plus methods for adding and removing callbacks on each of those reducers.
/// </summary>
public RemoteReducers Reducers => conn.Reducers;
/// <summary>
/// Access to setters for per-reducer flags.
///
/// The returned <c>SetReducerFlags</c> will have a method to invoke,
/// for each reducer defined by the module,
/// which call-flags for the reducer can be set.
/// </summary>
public SetReducerFlags SetReducerFlags => conn.SetReducerFlags;
/// <summary>
/// Access to procedures defined by the module.
///
/// The returned <c>RemoteProcedures</c> will have a method to invoke each procedure defined by the module,
/// with a callback for when the procedure completes and returns a value.
/// </summary>
public RemoteProcedures Procedures => conn.Procedures;
/// <summary>
/// Returns <c>true</c> if the connection is active, i.e. has not yet disconnected.
/// </summary>
public bool IsActive => conn.IsActive;
/// <summary>
/// Close the connection.
///
/// Throws an error if the connection is already closed.
/// </summary>
public void Disconnect()
{
conn.Disconnect();
}
/// <summary>
/// Start building a subscription.
/// </summary>
/// <returns>A builder-pattern constructor for subscribing to queries,
/// causing matching rows to be replicated into the client cache.</returns>
public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder();
/// <summary>
/// Get the <c>Identity</c> of this connection.
///
/// This method returns null if the connection was constructed anonymously
/// and we have not yet received our newly-generated <c>Identity</c> from the host.
/// </summary>
public Identity? Identity => conn.Identity;
/// <summary>
/// Get this connection's <c>ConnectionId</c>.
/// </summary>
public ConnectionId ConnectionId => conn.ConnectionId;
/// <summary>
/// Register a callback to be called when a reducer with no handler returns an error.
/// </summary>
public event Action<ReducerEventContext, Exception>? OnUnhandledReducerError
{
add => Reducers.InternalOnUnhandledReducerError += value;
remove => Reducers.InternalOnUnhandledReducerError -= value;
}
internal EventContext(DbConnection conn, Event<Reducer> Event)
{
this.conn = conn;
this.Event = Event;
}
}
public sealed class ReducerEventContext : IReducerEventContext, IRemoteDbContext
{
private readonly DbConnection conn;
/// <summary>
/// The reducer event that caused this callback to run.
/// </summary>
public readonly ReducerEvent<Reducer> Event;
/// <summary>
/// Access to tables in the client cache, which stores a read-only replica of the remote database state.
///
/// The returned <c>DbView</c> will have a method to access each table defined by the module.
/// </summary>
public RemoteTables Db => conn.Db;
/// <summary>
/// Access to reducers defined by the module.
///
/// The returned <c>RemoteReducers</c> will have a method to invoke each reducer defined by the module,
/// plus methods for adding and removing callbacks on each of those reducers.
/// </summary>
public RemoteReducers Reducers => conn.Reducers;
/// <summary>
/// Access to setters for per-reducer flags.
///
/// The returned <c>SetReducerFlags</c> will have a method to invoke,
/// for each reducer defined by the module,
/// which call-flags for the reducer can be set.
/// </summary>
public SetReducerFlags SetReducerFlags => conn.SetReducerFlags;
/// <summary>
/// Access to procedures defined by the module.
///
/// The returned <c>RemoteProcedures</c> will have a method to invoke each procedure defined by the module,
/// with a callback for when the procedure completes and returns a value.
/// </summary>
public RemoteProcedures Procedures => conn.Procedures;
/// <summary>
/// Returns <c>true</c> if the connection is active, i.e. has not yet disconnected.
/// </summary>
public bool IsActive => conn.IsActive;
/// <summary>
/// Close the connection.
///
/// Throws an error if the connection is already closed.
/// </summary>
public void Disconnect()
{
conn.Disconnect();
}
/// <summary>
/// Start building a subscription.
/// </summary>
/// <returns>A builder-pattern constructor for subscribing to queries,
/// causing matching rows to be replicated into the client cache.</returns>
public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder();
/// <summary>
/// Get the <c>Identity</c> of this connection.
///
/// This method returns null if the connection was constructed anonymously
/// and we have not yet received our newly-generated <c>Identity</c> from the host.
/// </summary>
public Identity? Identity => conn.Identity;
/// <summary>
/// Get this connection's <c>ConnectionId</c>.
/// </summary>
public ConnectionId ConnectionId => conn.ConnectionId;
/// <summary>
/// Register a callback to be called when a reducer with no handler returns an error.
/// </summary>
public event Action<ReducerEventContext, Exception>? OnUnhandledReducerError
{
add => Reducers.InternalOnUnhandledReducerError += value;
remove => Reducers.InternalOnUnhandledReducerError -= value;
}
internal ReducerEventContext(DbConnection conn, ReducerEvent<Reducer> reducerEvent)
{
this.conn = conn;
Event = reducerEvent;
}
}
public sealed class ErrorContext : IErrorContext, IRemoteDbContext
{
private readonly DbConnection conn;
/// <summary>
/// The <c>Exception</c> that caused this error callback to be run.
/// </summary>
public readonly Exception Event;
Exception IErrorContext.Event
{
get
{
return Event;
}
}
/// <summary>
/// Access to tables in the client cache, which stores a read-only replica of the remote database state.
///
/// The returned <c>DbView</c> will have a method to access each table defined by the module.
/// </summary>
public RemoteTables Db => conn.Db;
/// <summary>
/// Access to reducers defined by the module.
///
/// The returned <c>RemoteReducers</c> will have a method to invoke each reducer defined by the module,
/// plus methods for adding and removing callbacks on each of those reducers.
/// </summary>
public RemoteReducers Reducers => conn.Reducers;
/// <summary>
/// Access to setters for per-reducer flags.
///
/// The returned <c>SetReducerFlags</c> will have a method to invoke,
/// for each reducer defined by the module,
/// which call-flags for the reducer can be set.
/// </summary>
public SetReducerFlags SetReducerFlags => conn.SetReducerFlags;
/// <summary>
/// Access to procedures defined by the module.
///
/// The returned <c>RemoteProcedures</c> will have a method to invoke each procedure defined by the module,
/// with a callback for when the procedure completes and returns a value.
/// </summary>
public RemoteProcedures Procedures => conn.Procedures;
/// <summary>
/// Returns <c>true</c> if the connection is active, i.e. has not yet disconnected.
/// </summary>
public bool IsActive => conn.IsActive;
/// <summary>
/// Close the connection.
///
/// Throws an error if the connection is already closed.
/// </summary>
public void Disconnect()
{
conn.Disconnect();
}
/// <summary>
/// Start building a subscription.
/// </summary>
/// <returns>A builder-pattern constructor for subscribing to queries,
/// causing matching rows to be replicated into the client cache.</returns>
public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder();
/// <summary>
/// Get the <c>Identity</c> of this connection.
///
/// This method returns null if the connection was constructed anonymously
/// and we have not yet received our newly-generated <c>Identity</c> from the host.
/// </summary>
public Identity? Identity => conn.Identity;
/// <summary>
/// Get this connection's <c>ConnectionId</c>.
/// </summary>
public ConnectionId ConnectionId => conn.ConnectionId;
/// <summary>
/// Register a callback to be called when a reducer with no handler returns an error.
/// </summary>
public event Action<ReducerEventContext, Exception>? 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;
/// <summary>
/// Access to tables in the client cache, which stores a read-only replica of the remote database state.
///
/// The returned <c>DbView</c> will have a method to access each table defined by the module.
/// </summary>
public RemoteTables Db => conn.Db;
/// <summary>
/// Access to reducers defined by the module.
///
/// The returned <c>RemoteReducers</c> will have a method to invoke each reducer defined by the module,
/// plus methods for adding and removing callbacks on each of those reducers.
/// </summary>
public RemoteReducers Reducers => conn.Reducers;
/// <summary>
/// Access to setters for per-reducer flags.
///
/// The returned <c>SetReducerFlags</c> will have a method to invoke,
/// for each reducer defined by the module,
/// which call-flags for the reducer can be set.
/// </summary>
public SetReducerFlags SetReducerFlags => conn.SetReducerFlags;
/// <summary>
/// Access to procedures defined by the module.
///
/// The returned <c>RemoteProcedures</c> will have a method to invoke each procedure defined by the module,
/// with a callback for when the procedure completes and returns a value.
/// </summary>
public RemoteProcedures Procedures => conn.Procedures;
/// <summary>
/// Returns <c>true</c> if the connection is active, i.e. has not yet disconnected.
/// </summary>
public bool IsActive => conn.IsActive;
/// <summary>
/// Close the connection.
///
/// Throws an error if the connection is already closed.
/// </summary>
public void Disconnect()
{
conn.Disconnect();
}
/// <summary>
/// Start building a subscription.
/// </summary>
/// <returns>A builder-pattern constructor for subscribing to queries,
/// causing matching rows to be replicated into the client cache.</returns>
public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder();
/// <summary>
/// Get the <c>Identity</c> of this connection.
///
/// This method returns null if the connection was constructed anonymously
/// and we have not yet received our newly-generated <c>Identity</c> from the host.
/// </summary>
public Identity? Identity => conn.Identity;
/// <summary>
/// Get this connection's <c>ConnectionId</c>.
/// </summary>
public ConnectionId ConnectionId => conn.ConnectionId;
/// <summary>
/// Register a callback to be called when a reducer with no handler returns an error.
/// </summary>
public event Action<ReducerEventContext, Exception>? 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;
/// <summary>
/// The procedure event that caused this callback to run.
/// </summary>
public readonly ProcedureEvent Event;
/// <summary>
/// Access to tables in the client cache, which stores a read-only replica of the remote database state.
///
/// The returned <c>DbView</c> will have a method to access each table defined by the module.
/// </summary>
public RemoteTables Db => conn.Db;
/// <summary>
/// Access to reducers defined by the module.
///
/// The returned <c>RemoteReducers</c> will have a method to invoke each reducer defined by the module,
/// plus methods for adding and removing callbacks on each of those reducers.
/// </summary>
public RemoteReducers Reducers => conn.Reducers;
/// <summary>
/// Access to setters for per-reducer flags.
///
/// The returned <c>SetReducerFlags</c> will have a method to invoke,
/// for each reducer defined by the module,
/// which call-flags for the reducer can be set.
/// </summary>
public SetReducerFlags SetReducerFlags => conn.SetReducerFlags;
/// <summary>
/// Access to procedures defined by the module.
///
/// The returned <c>RemoteProcedures</c> will have a method to invoke each procedure defined by the module,
/// with a callback for when the procedure completes and returns a value.
/// </summary>
public RemoteProcedures Procedures => conn.Procedures;
/// <summary>
/// Returns <c>true</c> if the connection is active, i.e. has not yet disconnected.
/// </summary>
public bool IsActive => conn.IsActive;
/// <summary>
/// Close the connection.
///
/// Throws an error if the connection is already closed.
/// </summary>
public void Disconnect()
{
conn.Disconnect();
}
/// <summary>
/// Start building a subscription.
/// </summary>
/// <returns>A builder-pattern constructor for subscribing to queries,
/// causing matching rows to be replicated into the client cache.</returns>
public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder();
/// <summary>
/// Get the <c>Identity</c> of this connection.
///
/// This method returns null if the connection was constructed anonymously
/// and we have not yet received our newly-generated <c>Identity</c> from the host.
/// </summary>
public Identity? Identity => conn.Identity;
/// <summary>
/// Get this connection's <c>ConnectionId</c>.
/// </summary>
public ConnectionId ConnectionId => conn.ConnectionId;
/// <summary>
/// Register a callback to be called when a reducer with no handler returns an error.
/// </summary>
public event Action<ReducerEventContext, Exception>? OnUnhandledReducerError
{
add => Reducers.InternalOnUnhandledReducerError += value;
remove => Reducers.InternalOnUnhandledReducerError -= value;
}
internal ProcedureEventContext(DbConnection conn, ProcedureEvent Event)
{
this.conn = conn;
this.Event = Event;
}
}
/// <summary>
/// Builder-pattern constructor for subscription queries.
/// </summary>
public sealed class SubscriptionBuilder
{
private readonly IDbConnection conn;
private event Action<SubscriptionEventContext>? Applied;
private event Action<ErrorContext, Exception>? Error;
/// <summary>
/// Private API, use <c>conn.SubscriptionBuilder()</c> instead.
/// </summary>
public SubscriptionBuilder(IDbConnection conn)
{
this.conn = conn;
}
/// <summary>
/// Register a callback to run when the subscription is applied.
/// </summary>
public SubscriptionBuilder OnApplied(
Action<SubscriptionEventContext> callback
)
{
Applied += callback;
return this;
}
/// <summary>
/// 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 <c>Self::on_applied</c> will never run,
/// or later during the subscription's lifetime if the module's interface changes,
/// in which case <c>Self::on_applied</c> may have already run.
/// </summary>
public SubscriptionBuilder OnError(
Action<ErrorContext, Exception> callback
)
{
Error += callback;
return this;
}
/// <summary>
/// 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).
/// </summary>
public TypedSubscriptionBuilder AddQuery<TRow>(
Func<QueryBuilder, global::SpacetimeDB.Query<TRow>> build
)
{
var typed = new TypedSubscriptionBuilder(conn, Applied, Error);
return typed.AddQuery(build);
}
/// <summary>
/// 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:
/// <a href="https://spacetimedb.com/docs/sql">https://spacetimedb.com/docs/sql</a>
/// </summary>
public SubscriptionHandle Subscribe(
string[] querySqls
) => new(conn, Applied, Error, querySqls);
/// <summary>
/// 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 <c>Self.Subscribe</c>
/// in order to replicate only the subset of data which the client needs to function.
///
/// This method should not be combined with <c>Self.Subscribe</c> on the same <c>DbConnection</c>.
/// A connection may either <c>Self.Subscribe</c> to particular queries,
/// or <c>Self.SubscribeToAllTables</c>, but not both.
/// Attempting to call <c>Self.Subscribe</c>
/// on a <c>DbConnection</c> that has previously used <c>Self.SubscribeToAllTables</c>,
/// or vice versa, may misbehave in any number of ways,
/// including dropping subscriptions, corrupting the client cache, or panicking.
/// </summary>
public void SubscribeToAllTables()
{
// Make sure we use the legacy handle constructor here, even though there's only 1 query.
// We drop the error handler, since it can't be called for legacy subscriptions.
new SubscriptionHandle(
conn,
Applied,
new string[] { "SELECT * FROM *" }
);
}
}
public sealed class SubscriptionHandle : SubscriptionHandleBase<SubscriptionEventContext, ErrorContext>
{
/// <summary>
/// Internal API. Construct <c>SubscriptionHandle</c>s using <c>conn.SubscriptionBuilder</c>.
/// </summary>
public SubscriptionHandle(IDbConnection conn, Action<SubscriptionEventContext>? onApplied, string[] querySqls) : base(conn, onApplied, querySqls)
{ }
/// <summary>
/// Internal API. Construct <c>SubscriptionHandle</c>s using <c>conn.SubscriptionBuilder</c>.
/// </summary>
public SubscriptionHandle(
IDbConnection conn,
Action<SubscriptionEventContext>? onApplied,
Action<ErrorContext, Exception>? onError,
string[] querySqls
) : base(conn, onApplied, onError, querySqls)
{ }
}
public sealed class QueryBuilder
{
public From From { get; } = new();
}
public sealed class From
{
public global::SpacetimeDB.Table<Message, MessageCols, MessageIxCols> Message() => new("message", new MessageCols("message"), new MessageIxCols("message"));
public global::SpacetimeDB.Table<User, UserCols, UserIxCols> User() => new("user", new UserCols("user"), new UserIxCols("user"));
}
public sealed class TypedSubscriptionBuilder
{
private readonly IDbConnection conn;
private Action<SubscriptionEventContext>? Applied;
private Action<ErrorContext, Exception>? Error;
private readonly List<string> querySqls = new();
internal TypedSubscriptionBuilder(IDbConnection conn, Action<SubscriptionEventContext>? applied, Action<ErrorContext, Exception>? error)
{
this.conn = conn;
Applied = applied;
Error = error;
}
public TypedSubscriptionBuilder OnApplied(Action<SubscriptionEventContext> callback)
{
Applied += callback;
return this;
}
public TypedSubscriptionBuilder OnError(Action<ErrorContext, Exception> callback)
{
Error += callback;
return this;
}
public TypedSubscriptionBuilder AddQuery<TRow>(Func<QueryBuilder, global::SpacetimeDB.Query<TRow>> build)
{
var qb = new QueryBuilder();
querySqls.Add(build(qb).Sql);
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<DbConnection, RemoteTables, Reducer>
{
public override RemoteTables Db { get; }
public readonly RemoteReducers Reducers;
public readonly SetReducerFlags SetReducerFlags = new();
public readonly RemoteProcedures Procedures;
public DbConnection()
{
Db = new(this);
Reducers = new(this, SetReducerFlags);
Procedures = new(this);
}
protected override Reducer ToReducer(TransactionUpdate update)
{
var encodedArgs = update.ReducerCall.Args;
return update.ReducerCall.ReducerName switch
{
"ClientConnected" => BSATNHelpers.Decode<Reducer.ClientConnected>(encodedArgs),
"ClientDisconnected" => BSATNHelpers.Decode<Reducer.ClientDisconnected>(encodedArgs),
"SendMessage" => BSATNHelpers.Decode<Reducer.SendMessage>(encodedArgs),
"SetName" => BSATNHelpers.Decode<Reducer.SetName>(encodedArgs),
"" => throw new SpacetimeDBEmptyReducerNameException("Reducer name is empty"),
var reducer => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}")
};
}
protected override IEventContext ToEventContext(Event<Reducer> Event) =>
new EventContext(this, Event);
protected override IReducerEventContext ToReducerEventContext(ReducerEvent<Reducer> 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.ClientConnected args => Reducers.InvokeClientConnected(eventContext, args),
Reducer.ClientDisconnected args => Reducers.InvokeClientDisconnected(eventContext, args),
Reducer.SendMessage args => Reducers.InvokeSendMessage(eventContext, args),
Reducer.SetName args => Reducers.InvokeSetName(eventContext, args),
_ => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}")
};
}
public SubscriptionBuilder SubscriptionBuilder() => new(this);
public event Action<ReducerEventContext, Exception> OnUnhandledReducerError
{
add => Reducers.InternalOnUnhandledReducerError += value;
remove => Reducers.InternalOnUnhandledReducerError -= value;
}
}
}