Skip to content

Commit a39b81b

Browse files
[C#] Primary keys for query builder views (#4626)
# Description of Changes Same change set as #4614, just targeting master. The return type of a query builder view is now a special SATS product type `{ __query__: T }`. A view with this return type now has a primary key if `T` has a primary key. This means that client codegen will generate an `OnUpdate` callback for such views. It will also generate query builder index bindings for the primary key column. # API and ABI breaking changes None. Old modules with query builder views still work, they just don't have primary keys. # Expected complexity level and risk 2 # Testing Added equivalent tests to the ones that were added in #4573 and #4572.
1 parent cf06f8a commit a39b81b

34 files changed

Lines changed: 1445 additions & 161 deletions

crates/bindings-csharp/BSATN.Runtime/BSATN/AlgebraicType.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Unit F64
3939
)>
4040
{
4141
public static readonly AlgebraicType Unit = new Product([]);
42+
public const string QueryBuilderProductTypeTag = "__query__";
4243

4344
// Special AlgebraicType that can be recognised by the SpacetimeDB `generate` CLI as an Option<T>.
4445
internal static AlgebraicType MakeOption(AlgebraicType someType) =>
@@ -47,4 +48,8 @@ internal static AlgebraicType MakeOption(AlgebraicType someType) =>
4748
// Special AlgebraicType that can be recognised by the SpacetimeDB `generate` CLI as a Result<T, E>.
4849
internal static AlgebraicType MakeResult(AlgebraicType okType, AlgebraicType errType) =>
4950
new Sum([new("ok", okType), new("err", errType)]);
51+
52+
// Special AlgebraicType that can be recognised by the SpacetimeDB `generate` CLI as Query<T>.
53+
public static AlgebraicType MakeQueryBuilderProductType(Ref rowProductTypeRef) =>
54+
new Product([new(QueryBuilderProductTypeTag, rowProductTypeRef)]);
5055
}

crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,10 +1636,9 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar
16361636
IsPublic: true,
16371637
IsAnonymous: false,
16381638
Params: [],
1639-
ReturnType: new SpacetimeDB.BSATN.ValueOption<
1640-
PublicTable,
1641-
PublicTable.BSATN
1642-
>().GetAlgebraicType(registrar)
1639+
ReturnType: global::SpacetimeDB.BSATN.AlgebraicType.MakeQueryBuilderProductType(
1640+
new PublicTable.BSATN().GetAlgebraicType(registrar)
1641+
)
16431642
);
16441643

16451644
public byte[] Invoke(

crates/bindings-csharp/Codegen/Module.cs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,7 @@ record ViewDeclaration
11221122
public readonly bool IsPublic;
11231123
public readonly bool ReturnsQuery;
11241124
public readonly TypeUse ReturnType;
1125+
public readonly TypeUse? QueryRowType;
11251126
public readonly EquatableArray<MemberDeclaration> Parameters;
11261127
public readonly Scope Scope;
11271128

@@ -1186,15 +1187,12 @@ method.ReturnType is INamedTypeSymbol
11861187
{
11871188
ReturnsQuery = true;
11881189
var rowType = TypeUse.Parse(method, queryRowType, diag);
1189-
var optType = queryRowType.IsValueType
1190-
? "SpacetimeDB.BSATN.ValueOption"
1191-
: "SpacetimeDB.BSATN.RefOption";
1192-
var opt = $"{optType}<{rowType.Name}, {rowType.BSATNName}>";
1193-
// Match Rust semantics: Query<T> is described as a nullable row (T?).
1194-
ReturnType = new ReferenceUse(opt, opt);
1190+
QueryRowType = rowType;
1191+
ReturnType = rowType;
11951192
}
11961193
else
11971194
{
1195+
QueryRowType = null;
11981196
ReturnType = TypeUse.Parse(method, method.ReturnType, diag);
11991197
}
12001198
Scope = new Scope(methodSyntax.Parent as MemberDeclarationSyntax);
@@ -1211,9 +1209,10 @@ method.ReturnType is INamedTypeSymbol
12111209
diag.Report(ErrorDescriptor.ViewContextParam, methodSyntax);
12121210
}
12131211

1214-
// Validate return type: must be List<T> or T?
1212+
// Validate return type: must be List<T>, T?, or IQuery<T>.
12151213
if (
1216-
!ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.ValueOption")
1214+
!ReturnsQuery
1215+
&& !ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.ValueOption")
12171216
&& !ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.RefOption")
12181217
&& !ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.List")
12191218
)
@@ -1229,17 +1228,22 @@ method.ReturnType is INamedTypeSymbol
12291228
);
12301229
}
12311230

1232-
public string GenerateViewDef(uint Index) =>
1233-
$$$"""
1231+
public string GenerateViewDef(uint Index)
1232+
{
1233+
var returnTypeExpr = ReturnsQuery
1234+
? $"global::SpacetimeDB.BSATN.AlgebraicType.MakeQueryBuilderProductType(new {QueryRowType!.BSATNName}().GetAlgebraicType(registrar))"
1235+
: $"new {ReturnType.BSATNName}().GetAlgebraicType(registrar)";
1236+
return $$$"""
12341237
new global::SpacetimeDB.Internal.RawViewDefV10(
12351238
SourceName: "{{{Name}}}",
12361239
Index: {{{Index}}},
12371240
IsPublic: {{{IsPublic.ToString().ToLower()}}},
12381241
IsAnonymous: {{{IsAnonymous.ToString().ToLower()}}},
12391242
Params: [{{{MemberDeclaration.GenerateDefs(Parameters)}}}],
1240-
ReturnType: new {{{ReturnType.BSATNName}}}().GetAlgebraicType(registrar)
1243+
ReturnType: {{{returnTypeExpr}}}
12411244
);
12421245
""";
1246+
}
12431247

12441248
/// <summary>
12451249
/// Generates the class responsible for evaluating a view.

crates/codegen/src/csharp.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,13 @@ impl Lang for Csharp<'_> {
653653
}
654654
}
655655
}
656+
for (columns, constraints) in schema.backcompat_column_constraints() {
657+
if constraints.has_indexed() || constraints.has_unique() || constraints.has_primary_key() {
658+
for col_pos in columns.iter() {
659+
ix_col_positions.insert(col_pos.idx());
660+
}
661+
}
662+
}
656663

657664
writeln!(output, "public sealed class {cols_owner_name}Cols");
658665
indented_block(&mut output, |output| {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bin
2+
obj

modules/sdk-test-view-pk-cs/Lib.cs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
namespace SpacetimeDB.Sdk.Test.ViewPk;
2+
3+
using SpacetimeDB;
4+
5+
public static partial class Module
6+
{
7+
[SpacetimeDB.Table(Accessor = "view_pk_player", Public = true)]
8+
public partial struct ViewPkPlayer
9+
{
10+
[SpacetimeDB.PrimaryKey]
11+
public ulong id;
12+
public string name;
13+
}
14+
15+
[SpacetimeDB.Table(Accessor = "view_pk_membership", Public = true)]
16+
public partial struct ViewPkMembership
17+
{
18+
[SpacetimeDB.PrimaryKey]
19+
public ulong id;
20+
21+
[SpacetimeDB.Index.BTree]
22+
public ulong player_id;
23+
}
24+
25+
[SpacetimeDB.Table(Accessor = "view_pk_membership_secondary", Public = true)]
26+
public partial struct ViewPkMembershipSecondary
27+
{
28+
[SpacetimeDB.PrimaryKey]
29+
public ulong id;
30+
31+
[SpacetimeDB.Index.BTree]
32+
public ulong player_id;
33+
}
34+
35+
[SpacetimeDB.Reducer]
36+
public static void insert_view_pk_player(ReducerContext ctx, ulong id, string name)
37+
{
38+
ctx.Db.view_pk_player.Insert(new ViewPkPlayer { id = id, name = name });
39+
}
40+
41+
[SpacetimeDB.Reducer]
42+
public static void update_view_pk_player(ReducerContext ctx, ulong id, string name)
43+
{
44+
ctx.Db.view_pk_player.id.Update(new ViewPkPlayer { id = id, name = name });
45+
}
46+
47+
[SpacetimeDB.Reducer]
48+
public static void insert_view_pk_membership(ReducerContext ctx, ulong id, ulong player_id)
49+
{
50+
ctx.Db.view_pk_membership.Insert(new ViewPkMembership { id = id, player_id = player_id });
51+
}
52+
53+
[SpacetimeDB.Reducer]
54+
public static void insert_view_pk_membership_secondary(
55+
ReducerContext ctx,
56+
ulong id,
57+
ulong player_id
58+
)
59+
{
60+
ctx.Db.view_pk_membership_secondary.Insert(
61+
new ViewPkMembershipSecondary { id = id, player_id = player_id }
62+
);
63+
}
64+
65+
[SpacetimeDB.View(Accessor = "all_view_pk_players", Public = true)]
66+
public static IQuery<ViewPkPlayer> all_view_pk_players(ViewContext ctx)
67+
{
68+
return ctx.From.view_pk_player();
69+
}
70+
71+
[SpacetimeDB.View(Accessor = "sender_view_pk_players_a", Public = true)]
72+
public static IQuery<ViewPkPlayer> sender_view_pk_players_a(ViewContext ctx)
73+
{
74+
return ctx
75+
.From.view_pk_membership()
76+
.RightSemijoin(
77+
ctx.From.view_pk_player(),
78+
(membership, player) => membership.player_id.Eq(player.id)
79+
);
80+
}
81+
82+
[SpacetimeDB.View(Accessor = "sender_view_pk_players_b", Public = true)]
83+
public static IQuery<ViewPkPlayer> sender_view_pk_players_b(ViewContext ctx)
84+
{
85+
return ctx
86+
.From.view_pk_membership_secondary()
87+
.RightSemijoin(
88+
ctx.From.view_pk_player(),
89+
(membership, player) => membership.player_id.Eq(player.id)
90+
);
91+
}
92+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# `sdk-test-view-pk-cs` *C#* test
2+
3+
See the [sdk-test-view-pk README](../sdk-test-view-pk/README.md) for more details.
4+
5+
> **WARNING**: This C# source code is manually derived from `../sdk-test-view-pk/src/lib.rs`
6+
> and is supposed to be functionally equivalent.
7+
> Do not add new types or functionality here that are not present in the *Rust* version,
8+
> because they're compared against each other.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<!--
4+
Use local package sources instead of published ones.
5+
This makes integration test somewhat differ from production configuration, but
6+
at least it simplifies workflow for editing and testing C# code itself.
7+
-->
8+
<ItemGroup>
9+
<ProjectReference Include="../../crates/bindings-csharp/Codegen/Codegen.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
10+
<ProjectReference Include="../../crates/bindings-csharp/Runtime/Runtime.csproj" />
11+
</ItemGroup>
12+
13+
</Project>

0 commit comments

Comments
 (0)