mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-05-11 10:29:21 -04:00
f9ccf4c1c4
# Description of Changes This PR fixes a C# SDK regression where using `Bound` in index filters could trigger an ambiguous reference compiler error for Local after upgrading to `v1.11.2`, as reported in [#3995](https://github.com/clockworklabs/SpacetimeDB/issues/3995). It also fixes a related warning-spam regression (`CS0436`) where user projects could see `Local` type conflicts between generated module code and the `SpacetimeDB.Runtime` assembly. * Introduced a public `SpacetimeDB.Bound` type so users no longer need to import `SpacetimeDB.Internal` to use bounds in index filters. * Kept `SpacetimeDB.Internal.Bound` for compatibility, but added implicit conversions between `SpacetimeDB.Internal.Bound` and `SpacetimeDB.Bound`. * Updated the C# code generator to emit fully-qualified `global::SpacetimeDB.Bound` in generated index filter signatures, avoiding `SpacetimeDB.Internal` in public-facing APIs and preventing name collisions (e.g., `Local`). * Updated internal runtime bounds helpers (`BTreeIndexBounds<...>`) to explicitly use `SpacetimeDB.Bound` when constructing range-scan arguments. * Updated Codegen snapshot fixtures to match the new generated output (type name + formatting). * Fixed codegen output for `ITableView` `static abstract` member implementations to generate `public static` methods (required for the generated code to compile). It also fixes a related warning-spam regression (CS0436) where user projects could see Local type conflicts between generated module code and the SpacetimeDB.Runtime assembly. Additional fix (related to the `Local` reports): * Removed the runtime assembly’s ownership of `SpacetimeDB.Local` (introduced more recently than the generated module `Local`) to prevent `CS0436` duplicate-type warnings. Basically, the runtime’s concrete `Local`/`ProcedureTxContext` helpers were renamed and made internal so the code generator remains the sole owner of module-level `SpacetimeDB.Local`. Regression coverage: * Added generator regression assertions to ensure generated code does not reference `global::SpacetimeDB.Internal.Bound<...>` and does reference `global::SpacetimeDB.Bound<...>`. * Added a runtime API regression assertion that `SpacetimeDB.Bound` exists and is public in the runtime reference. * Added a regression assertion that `SpacetimeDB.Runtime` does not define codegen-owned types (e.g. `SpacetimeDB.Local`, `ProcedureContext`, etc.) to prevent future `CS0436` conflicts. * Added a “simulated downstream user file” compile check ensuring no `CS0436` diagnostics occur when user code references `SpacetimeDB.Local`. # API and ABI breaking changes None. * No schema or wire-format changes. * The changes are limited to C# type exposure / naming and codegen output. * `SpacetimeDB.Internal.Bound` remains usable via implicit conversions (backwards compatible for existing code). # Expected complexity level and risk 2 - Low * Changes are isolated to C# runtime type exposure, codegen type references, and snapshot updates. * No runtime behavior changes to index scan encoding/decoding; only avoids requiring SpacetimeDB.Internal in user code. # Testing - [X] Ran:`dotnet test crates/bindings-csharp/Codegen.Tests/Codegen.Tests.csproj` - [X] Ran regression tests locally.
270 lines
7.8 KiB
C#
270 lines
7.8 KiB
C#
namespace SpacetimeDB;
|
|
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
|
#pragma warning disable STDB_UNSTABLE
|
|
public abstract class ProcedureContextBase(
|
|
Identity sender,
|
|
ConnectionId? connectionId,
|
|
Random random,
|
|
Timestamp time
|
|
) : Internal.IInternalProcedureContext
|
|
{
|
|
public static Identity Identity => Internal.IProcedureContext.GetIdentity();
|
|
public Identity Sender { get; } = sender;
|
|
public ConnectionId? ConnectionId { get; } = connectionId;
|
|
public Random Rng { get; } = random;
|
|
public Timestamp Timestamp { get; private set; } = time;
|
|
public AuthCtx SenderAuth { get; } = AuthCtx.BuildFromSystemTables(connectionId, sender);
|
|
|
|
// NOTE: The host rejects procedure HTTP requests while a mut transaction is open
|
|
// (WOULD_BLOCK_TRANSACTION). Avoid calling `Http.*` inside WithTx.
|
|
public HttpClient Http { get; } = new();
|
|
|
|
// **Note:** must be 0..=u32::MAX
|
|
protected int CounterUuid = 0;
|
|
private Internal.TxContext? txContext;
|
|
private ProcedureTxContextBase? cachedUserTxContext;
|
|
|
|
protected abstract ProcedureTxContextBase CreateTxContext(Internal.TxContext inner);
|
|
protected internal abstract LocalBase CreateLocal();
|
|
|
|
private protected ProcedureTxContextBase RequireTxContext()
|
|
{
|
|
var inner =
|
|
txContext
|
|
?? throw new InvalidOperationException("Transaction context was not initialised.");
|
|
cachedUserTxContext ??= CreateTxContext(inner);
|
|
cachedUserTxContext.Refresh(inner);
|
|
return cachedUserTxContext;
|
|
}
|
|
|
|
public Internal.TxContext EnterTxContext(long timestampMicros)
|
|
{
|
|
var timestamp = new Timestamp(timestampMicros);
|
|
Timestamp = timestamp;
|
|
txContext =
|
|
txContext?.WithTimestamp(timestamp)
|
|
?? new Internal.TxContext(
|
|
CreateLocal(),
|
|
Sender,
|
|
ConnectionId,
|
|
timestamp,
|
|
SenderAuth,
|
|
Rng
|
|
);
|
|
return txContext;
|
|
}
|
|
|
|
public void ExitTxContext() => txContext = null;
|
|
|
|
public readonly struct TxOutcome<TResult>(bool isSuccess, TResult? value, Exception? error)
|
|
{
|
|
public bool IsSuccess { get; } = isSuccess;
|
|
public TResult? Value { get; } = value;
|
|
public Exception? Error { get; } = error;
|
|
|
|
public static TxOutcome<TResult> Success(TResult value) => new(true, value, null);
|
|
|
|
public static TxOutcome<TResult> Failure(Exception error) => new(false, default, error);
|
|
|
|
public TResult UnwrapOrThrow() =>
|
|
IsSuccess
|
|
? Value!
|
|
: throw (
|
|
Error
|
|
?? new InvalidOperationException("Transaction failed without an error object.")
|
|
);
|
|
|
|
public TResult UnwrapOrThrow(Func<Exception> fallbackFactory) =>
|
|
IsSuccess ? Value! : throw (Error ?? fallbackFactory());
|
|
}
|
|
|
|
[Experimental("STDB_UNSTABLE")]
|
|
public TResult WithTx<TResult>(Func<ProcedureTxContextBase, TResult> body) =>
|
|
TryWithTx(tx => Result<TResult, Exception>.Ok(body(tx))).UnwrapOrThrow();
|
|
|
|
[Experimental("STDB_UNSTABLE")]
|
|
public TxOutcome<TResult> TryWithTx<TResult, TError>(
|
|
Func<ProcedureTxContextBase, Result<TResult, TError>> body
|
|
)
|
|
where TError : Exception
|
|
{
|
|
try
|
|
{
|
|
var result = RunWithRetry(body);
|
|
|
|
return result switch
|
|
{
|
|
Result<TResult, TError>.OkR(var value) => TxOutcome<TResult>.Success(value),
|
|
Result<TResult, TError>.ErrR(var error) => TxOutcome<TResult>.Failure(error),
|
|
_ => throw new InvalidOperationException("Unknown Result variant."),
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return TxOutcome<TResult>.Failure(ex);
|
|
}
|
|
}
|
|
|
|
// Private transaction management methods (Rust-like encapsulation)
|
|
private long StartMutTx()
|
|
{
|
|
var status = Internal.FFI.procedure_start_mut_tx(out var micros);
|
|
Internal.FFI.ErrnoHelpers.ThrowIfError(status);
|
|
return micros;
|
|
}
|
|
|
|
private void CommitMutTx()
|
|
{
|
|
var status = Internal.FFI.procedure_commit_mut_tx();
|
|
Internal.FFI.ErrnoHelpers.ThrowIfError(status);
|
|
}
|
|
|
|
private void AbortMutTx()
|
|
{
|
|
var status = Internal.FFI.procedure_abort_mut_tx();
|
|
Internal.FFI.ErrnoHelpers.ThrowIfError(status);
|
|
}
|
|
|
|
private bool CommitMutTxWithRetry(Func<bool> retryBody)
|
|
{
|
|
try
|
|
{
|
|
CommitMutTx();
|
|
return true;
|
|
}
|
|
catch (TransactionNotAnonymousException)
|
|
{
|
|
return false;
|
|
}
|
|
catch (StdbException)
|
|
{
|
|
Log.Warn("Committing anonymous transaction failed; retrying once.");
|
|
if (retryBody())
|
|
{
|
|
CommitMutTx();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private Result<TResult, TError> RunWithRetry<TResult, TError>(
|
|
Func<ProcedureTxContextBase, Result<TResult, TError>> body
|
|
)
|
|
where TError : Exception
|
|
{
|
|
var result = RunOnce(body);
|
|
if (result is Result<TResult, TError>.ErrR)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
bool Retry()
|
|
{
|
|
result = RunOnce(body);
|
|
return result is Result<TResult, TError>.OkR;
|
|
}
|
|
|
|
if (!CommitMutTxWithRetry(Retry))
|
|
{
|
|
return result;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private Result<TResult, TError> RunOnce<TResult, TError>(
|
|
Func<ProcedureTxContextBase, Result<TResult, TError>> body
|
|
)
|
|
where TError : Exception
|
|
{
|
|
var micros = StartMutTx();
|
|
using var guard = new AbortGuard(AbortMutTx);
|
|
EnterTxContext(micros);
|
|
var txCtx = RequireTxContext();
|
|
|
|
Result<TResult, TError> result;
|
|
try
|
|
{
|
|
result = body(txCtx);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
throw;
|
|
}
|
|
|
|
if (result is Result<TResult, TError>.OkR)
|
|
{
|
|
guard.Disarm();
|
|
return result;
|
|
}
|
|
|
|
AbortMutTx();
|
|
guard.Disarm();
|
|
return result;
|
|
}
|
|
|
|
private sealed class AbortGuard(Action abort) : IDisposable
|
|
{
|
|
private readonly Action abort = abort;
|
|
private bool disarmed;
|
|
|
|
public void Disarm() => disarmed = true;
|
|
|
|
public void Dispose()
|
|
{
|
|
if (!disarmed)
|
|
{
|
|
abort();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public abstract class ProcedureTxContextBase(Internal.TxContext inner)
|
|
{
|
|
internal Internal.TxContext Inner { get; private set; } = inner;
|
|
|
|
internal void Refresh(Internal.TxContext inner) => Inner = inner;
|
|
|
|
public LocalBase Db => (LocalBase)Inner.Db;
|
|
public Identity Sender => Inner.Sender;
|
|
public ConnectionId? ConnectionId => Inner.ConnectionId;
|
|
public Timestamp Timestamp => Inner.Timestamp;
|
|
public AuthCtx SenderAuth => Inner.SenderAuth;
|
|
public Random Rng => Inner.Rng;
|
|
}
|
|
|
|
public abstract class LocalBase : Internal.Local { }
|
|
|
|
internal sealed partial class RuntimeProcedureContext(
|
|
Identity sender,
|
|
ConnectionId? connectionId,
|
|
Random random,
|
|
Timestamp timestamp
|
|
) : ProcedureContextBase(sender, connectionId, random, timestamp)
|
|
{
|
|
private readonly RuntimeLocal _db = new();
|
|
|
|
protected internal override LocalBase CreateLocal() => _db;
|
|
|
|
protected override ProcedureTxContextBase CreateTxContext(Internal.TxContext inner) =>
|
|
_cached ??= new RuntimeProcedureTxContext(inner);
|
|
|
|
private RuntimeProcedureTxContext? _cached;
|
|
}
|
|
|
|
internal sealed class RuntimeProcedureTxContext : ProcedureTxContextBase
|
|
{
|
|
internal RuntimeProcedureTxContext(Internal.TxContext inner)
|
|
: base(inner) { }
|
|
|
|
public new RuntimeLocal Db => (RuntimeLocal)base.Db;
|
|
}
|
|
|
|
internal sealed class RuntimeLocal : LocalBase { }
|
|
|
|
#pragma warning restore STDB_UNSTABLE
|