mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-05-12 18:57:51 -04:00
4f0a21f948
# 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