Skip to content

Conversation

@rekhoff
Copy link
Contributor

@rekhoff rekhoff commented Jan 9, 2026

Description of Changes

This PR implements the C# client-side typed query builder, as assigned in #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)
  • 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).
      C# usage examples (mirroring the proposal’s Rust examples)
  1. Typed subscription flow (no raw SQL)
void Subscribe(SpacetimeDB.Types.DbConnection conn)
{
    conn.SubscriptionBuilder()
        .OnApplied(ctx => { /* ... */ })
        .OnError((ctx, err) => { /* ... */ })
        .AddQuery(qb => qb.From.Users().All())
        .AddQuery(qb => qb.From.Players().All())
        .Subscribe();
}
  1. Typed WHERE filters and boolean composition
conn.SubscriptionBuilder()
    .OnApplied(ctx => { /* ... */ })
    .OnError((ctx, err) => { /* ... */ })
    .AddQuery(qb =>
        qb.From.Players().Where(p =>
            p.Name.Eq("alice")
                .And(p.IsOnline.Eq(true))
        )
    )
    .Subscribe();
  1. “Admin can see all, otherwise only self” (proposal’s “player” view logic, but client-side)
Identity self = /* ... */;

conn.SubscriptionBuilder()
    .AddQuery(qb =>
        qb.From.Players().Where(p =>
            p.Identity.Eq(self)
        )
    )
    .Subscribe();
  1. Index-column access for query construction (IxCols)
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

  • Ran run-regression-tests.sh successfully after regenerating C# bindings.
  • Ran C# unit tests using dotnet test sdks/csharp/tests~/tests.csproj -c Release
  • Added a new unit test suite (sdks/csharp/tests~/QueryBuilderTests.cs) validating:
    • Identifier quoting / escaping
    • Literal formatting (strings, bools, enums, SpacetimeDB primitive types)
    • NULL semantics for Eq/Neq
    • Boolean expression parenthesization (And/Or/Not)

@rekhoff rekhoff self-assigned this Jan 9, 2026
@rekhoff rekhoff marked this pull request as ready for review January 9, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants