Files
SpacetimeDB/crates/bindings-csharp/Codegen/Module.cs
T
Jason Larabie 87de672bb3 Fix C# module bindings codegen for namespaces with view dispatcher (#3668)
# Description of Changes

Closes: #3658 
With the work on Views we ran into trouble with the Return Type and
certain C# code layout where the types are defined inside the Module
class. This fixes the bug in our current approach but needs
consideration to possibly be changed entirely.

# API and ABI breaking changes

N/A

# Expected complexity level and risk

1 - Minor change

# Testing

- [x] Retested with the linked code with and without the namespace
- [x] Re-ran regression tests

---------

Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com>
Co-authored-by: joshua-spacetime <josh@clockworklabs.io>
Co-authored-by: rekhoff <r.ekhoff@clockworklabs.io>
2025-11-19 23:32:21 +00:00

1597 lines
64 KiB
C#

namespace SpacetimeDB.Codegen;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using SpacetimeDB.Internal;
using static Utils;
/// <summary>
/// Represents column attributes parsed from field attributes in table classes.
/// Used to track metadata like primary keys, unique constraints, and default values.
/// </summary>
/// <param name="Mask">Bitmask representing the column attributes (PrimaryKey, Unique, etc.)</param>
/// <param name="Table">Optional table name if the attribute is table-specific</param>
/// <param name="Value">Optional value for attributes like Default that carry additional data</param>
readonly record struct ColumnAttr(ColumnAttrs Mask, string? Table = null, string? Value = null)
{
// Maps attribute type names to their corresponding attribute types
private static readonly ImmutableDictionary<string, System.Type> AttrTypes = ImmutableArray
.Create(
typeof(AutoIncAttribute),
typeof(PrimaryKeyAttribute),
typeof(UniqueAttribute),
typeof(DefaultAttribute)
)
.ToImmutableDictionary(t => t.FullName!);
/// <summary>
/// Parses a Roslyn AttributeData into a ColumnAttr instance.
/// </summary>
/// <param name="attrData">The attribute data to parse</param>
/// <returns>A ColumnAttr instance representing the parsed attribute, or default if the attribute type is not recognized</returns>
public static ColumnAttr Parse(AttributeData attrData)
{
if (
attrData.AttributeClass is not { } attrClass
|| !AttrTypes.TryGetValue(attrClass.ToString(), out var attrType)
)
{
return default;
}
// Special handling for DefaultAttribute as it contains an additional value
if (attrClass.ToString() == typeof(DefaultAttribute).FullName)
{
var defaultAttr = attrData.ParseAs<DefaultAttribute>(attrType);
return new(defaultAttr.Mask, defaultAttr.Table, defaultAttr.Value);
}
// Handle standard column attributes (PrimaryKey, Unique, AutoInc)
var attr = attrData.ParseAs<ColumnAttribute>(attrType);
return new(attr.Mask, attr.Table);
}
}
/// <summary>
/// Represents a reference to a column in a table, combining its index and name.
/// Used to maintain references to columns for indexing and querying purposes.
/// </summary>
/// <param name="Index">The zero-based index of the column in the table</param>
/// <param name="Name">The name of the column as defined in the source code</param>
record ColumnRef(int Index, string Name);
/// <summary>
/// Represents the declaration of a column in a table.
/// Contains metadata and attributes for the column, including its type, constraints, and indexes.
/// </summary>
record ColumnDeclaration : MemberDeclaration
{
public readonly EquatableArray<ColumnAttr> Attrs;
public readonly EquatableArray<TableIndex> Indexes;
public readonly bool IsEquatable;
public readonly string FullTableName;
public readonly int ColumnIndex;
public readonly string? ColumnDefaultValue;
// A helper to combine multiple column attributes into a single mask.
// Note: it doesn't check the table names, this is left up to the caller.
private static ColumnAttrs CombineColumnAttrs(IEnumerable<ColumnAttr> attrs) =>
attrs.Aggregate(ColumnAttrs.UnSet, (mask, attr) => mask | attr.Mask);
public ColumnDeclaration(string tableName, int index, IFieldSymbol field, DiagReporter diag)
: base(field, diag)
{
FullTableName = tableName;
ColumnIndex = index;
Attrs = new(
field
.GetAttributes()
.Select(ColumnAttr.Parse)
.Where(a => a.Mask != ColumnAttrs.UnSet)
.GroupBy(
a => a.Table,
(key, group) => new ColumnAttr(CombineColumnAttrs(group), key)
)
.ToImmutableArray()
);
Indexes = new(
field
.GetAttributes()
.Where(TableIndex.CanParse)
.Select(a => new TableIndex(new ColumnRef(index, field.Name), a, diag))
.ToImmutableArray()
);
ColumnDefaultValue = field
.GetAttributes()
.Select(ColumnAttr.Parse)
.Where(a => a.Mask == ColumnAttrs.Default)
.Select(a => a.Value)
.ToList()
.FirstOrDefault();
var type = field.Type;
var isInteger = type.SpecialType switch
{
SpecialType.System_Byte
or SpecialType.System_SByte
or SpecialType.System_Int16
or SpecialType.System_UInt16
or SpecialType.System_Int32
or SpecialType.System_UInt32
or SpecialType.System_Int64
or SpecialType.System_UInt64 => true,
SpecialType.None => type.ToString()
is "System.Int128"
or "System.UInt128"
or "SpacetimeDB.I128"
or "SpacetimeDB.U128"
or "SpacetimeDB.I256"
or "SpacetimeDB.U256",
_ => false,
};
var attrs = CombineColumnAttrs(Attrs);
if (attrs.HasFlag(ColumnAttrs.AutoInc) && !isInteger)
{
diag.Report(ErrorDescriptor.AutoIncNotInteger, field);
}
// Check whether this is a sum type without a payload.
var isAllUnitEnum = false;
if (type.TypeKind == Microsoft.CodeAnalysis.TypeKind.Enum)
{
isAllUnitEnum = true;
}
else if (type.BaseType?.OriginalDefinition.ToString() == "SpacetimeDB.TaggedEnum<Variants>")
{
if (
type.BaseType.TypeArguments.FirstOrDefault() is INamedTypeSymbol
{
IsTupleType: true,
TupleElements: var taggedEnumVariants
}
)
{
isAllUnitEnum = taggedEnumVariants.All(
(field) => field.Type.ToString() == "SpacetimeDB.Unit"
);
}
}
IsEquatable =
(
isInteger
|| isAllUnitEnum
|| type.SpecialType switch
{
SpecialType.System_String or SpecialType.System_Boolean => true,
SpecialType.None => type.ToString()
is "SpacetimeDB.ConnectionId"
or "SpacetimeDB.Identity",
_ => false,
}
)
&& type.NullableAnnotation != NullableAnnotation.Annotated;
if (attrs.HasFlag(ColumnAttrs.Unique) && !IsEquatable)
{
diag.Report(ErrorDescriptor.UniqueNotEquatable, field);
}
if (
attrs.HasFlag(ColumnAttrs.Default)
&& (
attrs.HasFlag(ColumnAttrs.AutoInc)
|| attrs.HasFlag(ColumnAttrs.PrimaryKey)
|| attrs.HasFlag(ColumnAttrs.Unique)
)
)
{
diag.Report(ErrorDescriptor.IncompatibleDefaultAttributesCombination, field);
}
}
public ColumnAttrs GetAttrs(TableAccessor tableAccessor) =>
CombineColumnAttrs(Attrs.Where(x => x.Table == null || x.Table == tableAccessor.Name));
// For the `TableDesc` constructor.
public string GenerateColumnDef() =>
$"new (nameof({Name}), BSATN.{Name}{TypeUse.BsatnFieldSuffix}.GetAlgebraicType(registrar))";
}
record Scheduled(string ReducerName, int ScheduledAtColumn);
record TableAccessor
{
public readonly string Name;
public readonly bool IsPublic;
public readonly Scheduled? Scheduled;
public TableAccessor(TableDeclaration table, AttributeData data, DiagReporter diag)
{
var attr = data.ParseAs<TableAttribute>();
Name = attr.Name ?? table.ShortName;
IsPublic = attr.Public;
if (
attr.Scheduled is { } reducer
&& table.GetColumnIndex(data, attr.ScheduledAt, diag) is { } scheduledAtIndex
)
{
try
{
Scheduled = new(reducer, scheduledAtIndex);
if (
table.GetPrimaryKey(this) is not { } pk
|| table.Members[pk].Type.Name != "ulong"
)
{
throw new InvalidOperationException(
$"{Name} is a scheduled table but doesn't have a primary key of type `ulong`."
);
}
if (
table.Members[Scheduled.ScheduledAtColumn].Type.Name != "SpacetimeDB.ScheduleAt"
)
{
throw new InvalidOperationException(
$"{Name}.{attr.ScheduledAt} is marked with `ScheduledAt`, but doesn't have the expected type `SpacetimeDB.ScheduleAt`."
);
}
}
catch (Exception e)
{
diag.Report(ErrorDescriptor.InvalidScheduledDeclaration, (data, e.Message));
}
}
}
}
enum TableIndexType
{
BTree,
}
/// <summary>
/// Represents an index on a database table accessor, used to optimize queries.
/// Supports B-tree indexing (and potentially other types in the future).
/// </summary>
record TableIndex
{
public readonly EquatableArray<ColumnRef> Columns;
public readonly string? Table;
public readonly string AccessorName;
public readonly TableIndexType Type;
// See: bindings_sys::index_id_from_name for documentation of this format.
// Guaranteed not to contain quotes, so does not need to be escaped when embedded in a string.
private readonly string StandardNameSuffix;
/// <summary>
/// Primary constructor that initializes all fields.
/// Other constructors delegate to this one to avoid code duplication.
/// </summary>
/// <param name="accessorName">Name to use when accessing this index. If null, will be generated from column names.</param>
/// <param name="columns">The columns that make up this index.</param>
/// <param name="tableName">The name of the table this index belongs to, if any.</param>
/// <param name="type">The type of index (currently only B-tree is supported).</param>
private TableIndex(
string? accessorName,
ImmutableArray<ColumnRef> columns,
string? tableName,
TableIndexType type
)
{
Columns = new(columns);
Table = tableName;
var columnNames = string.Join("_", columns.Select(c => c.Name));
AccessorName = accessorName ?? columnNames;
Type = type;
StandardNameSuffix = $"_{columnNames}_idx_{Type.ToString().ToLower()}";
}
/// <summary>
/// Creates a B-tree index on a single column with auto-generated name.
/// </summary>
/// <param name="col">The column to index.</param>
public TableIndex(ColumnRef col)
: this(
null,
ImmutableArray.Create(col),
null,
TableIndexType.BTree // this might become hash in the future
) { }
/// <summary>
/// Creates an index with the given attribute and columns.
/// Used internally by other constructors that parse attributes.
/// </summary>
private TableIndex(Index.BTreeAttribute attr, ImmutableArray<ColumnRef> columns)
: this(attr.Name, columns, attr.Table, TableIndexType.BTree) { }
/// <summary>
/// Creates an index from a table declaration and attribute data.
/// Validates the index configuration and reports any errors through the diag reporter.
/// </summary>
private TableIndex(
TableDeclaration table,
Index.BTreeAttribute attr,
AttributeData data,
DiagReporter diag
)
: this(
attr,
attr.Columns.Select(name => new ColumnRef(
table.GetColumnIndex(data, name, diag) ?? -1,
name
))
.Where(c => c.Index != -1)
.ToImmutableArray()
)
{
if (attr.Columns.Length == 0)
{
diag.Report(ErrorDescriptor.EmptyIndexColumns, data);
}
}
/// <summary>
/// Creates an index by parsing attribute data from a table declaration.
/// </summary>
public TableIndex(TableDeclaration table, AttributeData data, DiagReporter diag)
: this(table, data.ParseAs<Index.BTreeAttribute>(), data, diag) { }
/// <summary>
/// Creates an index for a single column with attribute data.
/// Validates that no additional columns were specified in the attribute.
/// </summary>
private TableIndex(
ColumnRef column,
Index.BTreeAttribute attr,
AttributeData data,
DiagReporter diag
)
: this(attr, ImmutableArray.Create(column))
{
if (attr.Columns.Length != 0)
{
diag.Report(ErrorDescriptor.UnexpectedIndexColumns, data);
}
}
/// <summary>
/// Creates an index for a single column by parsing attribute data.
/// </summary>
public TableIndex(ColumnRef col, AttributeData data, DiagReporter diag)
: this(col, data.ParseAs<Index.BTreeAttribute>(), data, diag) { }
// `FullName` and Roslyn have different ways of representing nested types in full names -
// one uses a `Parent+Child` syntax, the other uses `Parent.Child`.
// Manually fixup one to the other.
private static readonly string BTreeAttrName = typeof(Index.BTreeAttribute).FullName.Replace(
'+',
'.'
);
public static bool CanParse(AttributeData data) =>
data.AttributeClass?.ToString() == BTreeAttrName;
public string GenerateIndexDef() =>
$$"""
new(
Name: null,
AccessorName: "{{AccessorName}}",
Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.{{Type}}([{{string.Join(
", ",
Columns.Select(c => c.Index)
)}}])
)
""";
public string StandardIndexName(TableAccessor tableAccessor) =>
tableAccessor.Name + StandardNameSuffix;
}
/// <summary>
/// Represents a table declaration in a module.
/// Handles table metadata, accessors, indexes, and column declarations for code generation.
/// </summary>
record TableDeclaration : BaseTypeDeclaration<ColumnDeclaration>
{
public readonly Accessibility Visibility;
public readonly EquatableArray<TableAccessor> TableAccessors;
public readonly EquatableArray<TableIndex> Indexes;
public int? GetColumnIndex(AttributeData attrContext, string name, DiagReporter diag)
{
var index = Members
.Select((col, i) => (col, i))
.FirstOrDefault(pair => pair.col.Name == name);
if (index.col is null)
{
diag.Report(ErrorDescriptor.UnknownColumn, (attrContext, name, ShortName));
return null;
}
return index.i;
}
public TableDeclaration(GeneratorAttributeSyntaxContext context, DiagReporter diag)
: base(context, diag)
{
var typeSyntax = (TypeDeclarationSyntax)context.TargetNode;
if (Kind is TypeKind.Sum)
{
diag.Report(ErrorDescriptor.TableTaggedEnum, typeSyntax);
}
var container = context.TargetSymbol;
Visibility = container.DeclaredAccessibility;
while (container != null)
{
switch (container.DeclaredAccessibility)
{
case Accessibility.ProtectedAndInternal:
case Accessibility.NotApplicable:
case Accessibility.Internal:
case Accessibility.Public:
if (Visibility < container.DeclaredAccessibility)
{
Visibility = container.DeclaredAccessibility;
}
break;
default:
diag.Report(ErrorDescriptor.InvalidTableVisibility, typeSyntax);
throw new Exception(
"Table row type visibility must be public or internal, including containing types."
);
}
container = container.ContainingType;
}
TableAccessors = new(
context.Attributes.Select(a => new TableAccessor(this, a, diag)).ToImmutableArray()
);
Indexes = new(
context
.TargetSymbol.GetAttributes()
.Where(TableIndex.CanParse)
.Select(a => new TableIndex(this, a, diag))
.ToImmutableArray()
);
}
protected override ColumnDeclaration ConvertMember(
int index,
IFieldSymbol field,
DiagReporter diag
) => new(FullName, index, field, diag);
public IEnumerable<string> GenerateTableAccessorFilters(TableAccessor tableAccessor)
{
var vis = SyntaxFacts.GetText(Visibility);
var globalName = $"global::{FullName}";
foreach (var ct in GetConstraints(tableAccessor, ColumnAttrs.Unique))
{
var f = ct.Col;
if (!f.IsEquatable)
{
// Skip - we already emitted diagnostic for this during parsing, and generated code would
// only produce a lot of noisy typechecking errors.
continue;
}
var standardIndexName = ct.ToIndex().StandardIndexName(tableAccessor);
yield return $$"""
{{vis}} sealed class {{f.Name}}UniqueIndex : UniqueIndex<{{tableAccessor.Name}}, {{globalName}}, {{f.Type.Name}}, {{f.Type.BSATNName}}> {
internal {{f.Name}}UniqueIndex() : base("{{standardIndexName}}") {}
// Important: don't move this to the base class.
// C# generics don't play well with nullable types and can't accept both struct-type-based and class-type-based
// `globalName` in one generic definition, leading to buggy `Row?` expansion for either one or another.
public {{globalName}}? Find({{f.Type.Name}} key) => DoFilter(key).Cast<{{globalName}}?>().SingleOrDefault();
public {{globalName}} Update({{globalName}} row) => DoUpdate(row);
}
{{vis}} {{f.Name}}UniqueIndex {{f.Name}} => new();
""";
}
foreach (var index in GetIndexes(tableAccessor))
{
var name = index.AccessorName;
// Skip bad declarations. Empty name means no columns, which we have already reported with a meaningful error.
// Emitting this will result in further compilation errors due to missing property name.
if (name == "")
{
continue;
}
var members = index.Columns.Select(c => Members[c.Index]).ToArray();
var standardIndexName = index.StandardIndexName(tableAccessor);
yield return $$"""
{{vis}} sealed class {{name}}Index() : SpacetimeDB.Internal.IndexBase<{{globalName}}>("{{standardIndexName}}") {
""";
for (var n = 0; n < members.Length; n++)
{
var types = string.Join(
", ",
members.Take(n + 1).Select(m => $"{m.Type.Name}, {m.Type.BSATNName}")
);
var scalars = members.Take(n).Select(m => $"{m.Type.Name} {m.Name}");
var lastScalar = $"{members[n].Type.Name} {members[n].Name}";
var lastBounds = $"Bound<{members[n].Type.Name}> {members[n].Name}";
var argsScalar = string.Join(", ", scalars.Append(lastScalar));
var argsBounds = string.Join(", ", scalars.Append(lastBounds));
string argName;
if (n > 0)
{
argName = "f";
argsScalar = $"({argsScalar}) f";
argsBounds = $"({argsBounds}) f";
}
else
{
argName = members[0].Name;
}
yield return $$"""
public IEnumerable<{{globalName}}> Filter({{argsScalar}}) =>
DoFilter(new SpacetimeDB.Internal.BTreeIndexBounds<{{types}}>({{argName}}));
public ulong Delete({{argsScalar}}) =>
DoDelete(new SpacetimeDB.Internal.BTreeIndexBounds<{{types}}>({{argName}}));
public IEnumerable<{{globalName}}> Filter({{argsBounds}}) =>
DoFilter(new SpacetimeDB.Internal.BTreeIndexBounds<{{types}}>({{argName}}));
public ulong Delete({{argsBounds}}) =>
DoDelete(new SpacetimeDB.Internal.BTreeIndexBounds<{{types}}>({{argName}}));
""";
}
yield return $"}}\n {vis} {name}Index {name} => new();\n";
}
}
private IEnumerable<string> GenerateReadOnlyAccessorFilters(TableAccessor tableAccessor)
{
var vis = SyntaxFacts.GetText(Visibility);
var globalName = $"global::{FullName}";
foreach (var ct in GetConstraints(tableAccessor, ColumnAttrs.Unique))
{
var f = ct.Col;
if (!f.IsEquatable)
{
continue;
}
var standardIndexName = ct.ToIndex().StandardIndexName(tableAccessor);
yield return $$$"""
public sealed class {{{f.Name}}}Index
: global::SpacetimeDB.Internal.ReadOnlyUniqueIndex<
global::SpacetimeDB.Internal.ViewHandles.{{{tableAccessor.Name}}}ReadOnly,
{{{globalName}}},
{{{f.Type.Name}}},
{{{f.Type.BSATNName}}}>
{
internal {{{f.Name}}}Index() : base("{{{standardIndexName}}}") { }
public {{{globalName}}}? Find({{{f.Type.Name}}} key) => FindSingle(key);
}
public {{{f.Name}}}Index {{{f.Name}}} => new();
""";
}
foreach (var index in GetIndexes(tableAccessor))
{
if (string.IsNullOrEmpty(index.AccessorName))
{
continue;
}
var members = index.Columns.Select(c => Members[c.Index]).ToArray();
var standardIndexName = index.StandardIndexName(tableAccessor);
var name = index.AccessorName;
var blocks = new List<string>
{
$$$"""
public sealed class {{{name}}}Index
: global::SpacetimeDB.Internal.ReadOnlyIndexBase<{{{globalName}}}>
{
internal {{{name}}}Index() : base("{{{standardIndexName}}}") {}
""",
};
for (var n = 0; n < members.Length; n++)
{
var declaringMembers = members.Take(n + 1).ToArray();
var types = string.Join(
", ",
declaringMembers.Select(m => $"{m.Type.Name}, {m.Type.BSATNName}")
);
var scalarArgs = string.Join(
", ",
declaringMembers.Select(m => $"{m.Type.Name} {m.Name}")
);
var boundsArgs = string.Join(
", ",
declaringMembers
.Take(n)
.Select(m => $"{m.Type.Name} {m.Name}")
.Append(
$"global::SpacetimeDB.Internal.Bound<{declaringMembers[^1].Type.Name}> {declaringMembers[^1].Name}"
)
);
var ctorArg = n == 0 ? declaringMembers[0].Name : "f";
if (n > 0)
{
scalarArgs = $"({scalarArgs}) f";
boundsArgs = $"({boundsArgs}) f";
}
blocks.Add(
$$$"""
public IEnumerable<{{{globalName}}}> Filter({{{scalarArgs}}}) =>
DoFilter(new global::SpacetimeDB.Internal.BTreeIndexBounds<{{{types}}}>({{{ctorArg}}}));
public IEnumerable<{{{globalName}}}> Filter({{{boundsArgs}}}) =>
DoFilter(new global::SpacetimeDB.Internal.BTreeIndexBounds<{{{types}}}>({{{ctorArg}}}));
"""
);
}
blocks.Add($"}}\n{vis} {name}Index {name} => new();");
yield return string.Join("\n", blocks);
}
}
/// <summary>
/// Represents a generated accessor for a table, providing different access patterns
/// and visibility levels for the underlying table data.
/// </summary>
/// <param name="tableAccessorName">Name of the generated accessor type</param>
/// <param name="tableName">Fully qualified name of the table type</param>
/// <param name="tableAccessor">C# source code for the accessor implementation</param>
/// <param name="getter">C# property getter for accessing the accessor</param>
public record struct GeneratedTableAccessor(
string tableAccessorName,
string tableName,
string tableAccessor,
string getter
);
/// <summary>
/// Generates accessor implementations for all table accessors defined in this table declaration.
/// Each accessor represents a different way to access or filter the table's data.
/// </summary>
/// <returns>Collection of Accessor records containing generated code for each accessor</returns>
public IEnumerable<GeneratedTableAccessor> GenerateTableAccessors()
{
// Don't try to generate accessors if this table is a sum type.
// We already emitted a diagnostic, and attempting to generate accessors will only result in more noisy errors.
if (Kind is TypeKind.Sum)
{
yield break;
}
foreach (var v in TableAccessors)
{
var autoIncFields = Members.Where(m => m.GetAttrs(v).HasFlag(ColumnAttrs.AutoInc));
var globalName = $"global::{FullName}";
var iTable = $"global::SpacetimeDB.Internal.ITableView<{v.Name}, {globalName}>";
yield return new(
v.Name,
globalName,
$$$"""
{{{SyntaxFacts.GetText(Visibility)}}} readonly struct {{{v.Name}}} : {{{iTable}}} {
static {{{globalName}}} {{{iTable}}}.ReadGenFields(System.IO.BinaryReader reader, {{{globalName}}} row) {
{{{string.Join(
"\n",
autoIncFields.Select(m =>
$$"""
if (row.{{m.Name}} == default)
{
row.{{m.Name}} = {{globalName}}.BSATN.{{m.Name}}{{TypeUse.BsatnFieldSuffix}}.Read(reader);
}
"""
)
)}}}
return row;
}
static SpacetimeDB.Internal.RawTableDefV9 {{{iTable}}}.MakeTableDesc(SpacetimeDB.BSATN.ITypeRegistrar registrar) => new (
Name: nameof({{{v.Name}}}),
ProductTypeRef: (uint) new {{{globalName}}}.BSATN().GetAlgebraicType(registrar).Ref_,
PrimaryKey: [{{{GetPrimaryKey(v)?.ToString() ?? ""}}}],
Indexes: [
{{{string.Join(
",\n",
GetConstraints(v, ColumnAttrs.Unique)
.Select(c => c.ToIndex())
.Concat(GetIndexes(v))
.Select(b => b.GenerateIndexDef())
)}}}
],
Constraints: {{{GenConstraintList(v, ColumnAttrs.Unique, $"{iTable}.MakeUniqueConstraint")}}},
Sequences: {{{GenConstraintList(v, ColumnAttrs.AutoInc, $"{iTable}.MakeSequence")}}},
Schedule: {{{(
v.Scheduled is { } scheduled
? $"{iTable}.MakeSchedule(\"{scheduled.ReducerName}\", {scheduled.ScheduledAtColumn})"
: "null"
)}}},
TableType: SpacetimeDB.Internal.TableType.User,
TableAccess: SpacetimeDB.Internal.TableAccess.{{{(v.IsPublic ? "Public" : "Private")}}}
);
public ulong Count => {{{iTable}}}.DoCount();
public IEnumerable<{{{globalName}}}> Iter() => {{{iTable}}}.DoIter();
public {{{globalName}}} Insert({{{globalName}}} row) => {{{iTable}}}.DoInsert(row);
public bool Delete({{{globalName}}} row) => {{{iTable}}}.DoDelete(row);
{{{string.Join("\n", GenerateTableAccessorFilters(v))}}}
}
""",
$"{SyntaxFacts.GetText(Visibility)} global::SpacetimeDB.Internal.TableHandles.{v.Name} {v.Name} => new();"
);
}
}
public record struct GeneratedReadOnlyAccessor(
string tableAccessorName,
string tableName,
string readOnlyAccessor,
string readOnlyGetter
);
public IEnumerable<GeneratedReadOnlyAccessor> GenerateReadOnlyAccessors()
{
if (Kind is TypeKind.Sum)
{
yield break;
}
foreach (var accessor in TableAccessors)
{
var globalName = $"global::{FullName}";
var readOnlyIndexDecls = string.Join("\n", GenerateReadOnlyAccessorFilters(accessor));
var visibility = SyntaxFacts.GetText(Visibility);
yield return new(
accessor.Name,
globalName,
$$$"""
{{{visibility}}} sealed class {{{accessor.Name}}}ReadOnly
: global::SpacetimeDB.Internal.ReadOnlyTableView<{{{globalName}}}>
{
internal {{{accessor.Name}}}ReadOnly() : base("{{{accessor.Name}}}") { }
public ulong Count => DoCount();
{{{readOnlyIndexDecls}}}
}
""",
$"{visibility} global::SpacetimeDB.Internal.ViewHandles.{accessor.Name}ReadOnly {accessor.Name} => new();"
);
}
}
/// <summary>
/// Represents a default value for a table field, used during table creation.
/// </summary>
/// <param name="tableName">Name of the table containing the field</param>
/// <param name="columnId">Index of the column in the table</param>
/// <param name="value">String representation of the default value</param>
/// <param name="BSATNTypeName">BSATN Type name of the default value</param>
public record struct FieldDefaultValue(
string tableName,
string columnId,
string value,
string BSATNTypeName
);
/// <summary>
/// Generates default values for table fields with the [Default] attribute.
/// These values are used when creating new rows without explicit values for the corresponding fields.
/// </summary>
/// <returns>Collection of default values for fields that specify them</returns>
public IEnumerable<FieldDefaultValue> GenerateDefaultValues()
{
if (Kind is TypeKind.Sum)
{
yield break;
}
foreach (var tableAccessor in TableAccessors)
{
var members = string.Join(", ", Members.Select(m => m.Name));
var fieldsWithDefaultValues = Members.Where(m =>
m.GetAttrs(tableAccessor).HasFlag(ColumnAttrs.Default)
);
var defaultValueAttributes = string.Join(
", ",
Members
.Where(m => m.GetAttrs(tableAccessor).HasFlag(ColumnAttrs.Default))
.Select(m => m.Attrs.FirstOrDefault(a => a.Mask == ColumnAttrs.Default))
);
var withDefaultValues =
fieldsWithDefaultValues as ColumnDeclaration[] ?? fieldsWithDefaultValues.ToArray();
foreach (var fieldsWithDefaultValue in withDefaultValues)
{
if (
fieldsWithDefaultValue.ColumnDefaultValue != null
&& fieldsWithDefaultValue.Type.BSATNName != ""
)
{
// For enums, we'll need to wrap the default value in the enum type.
if (fieldsWithDefaultValue.Type.BSATNName.StartsWith("SpacetimeDB.BSATN.Enum"))
{
yield return new FieldDefaultValue(
tableAccessor.Name,
fieldsWithDefaultValue.ColumnIndex.ToString(),
$"({fieldsWithDefaultValue.Type.Name}){fieldsWithDefaultValue.ColumnDefaultValue}",
fieldsWithDefaultValue.Type.BSATNName
);
}
else
{
yield return new FieldDefaultValue(
tableAccessor.Name,
fieldsWithDefaultValue.ColumnIndex.ToString(),
fieldsWithDefaultValue.ColumnDefaultValue,
fieldsWithDefaultValue.Type.BSATNName
);
}
}
}
}
}
public record Constraint(ColumnDeclaration Col, int Pos, ColumnAttrs Attr)
{
public TableIndex ToIndex() => new(new ColumnRef(Pos, Col.Name));
}
public IEnumerable<Constraint> GetConstraints(
TableAccessor tableAccessor,
ColumnAttrs filterByAttr = ~ColumnAttrs.UnSet
) =>
Members
// Important: the position must be stored here, before filtering.
.Select((col, pos) => new Constraint(col, pos, col.GetAttrs(tableAccessor)))
.Where(c => c.Attr.HasFlag(filterByAttr));
public IEnumerable<TableIndex> GetIndexes(TableAccessor tableAccessor) =>
Indexes
.Concat(Members.SelectMany(m => m.Indexes))
.Where(i => i.Table == null || i.Table == tableAccessor.Name);
// Reimplementation of V8 -> V9 constraint conversion in Rust.
// See https://github.com/clockworklabs/SpacetimeDB/blob/13a800e9f88cbe885b98eab9e45b0fcfd3ab7014/crates/schema/src/def/validate/v8.rs#L74-L78
// and https://github.com/clockworklabs/SpacetimeDB/blob/13a800e9f88cbe885b98eab9e45b0fcfd3ab7014/crates/lib/src/db/raw_def/v8.rs#L460-L510
private string GenConstraintList(
TableAccessor tableAccessor,
ColumnAttrs filterByAttr,
string makeConstraintFn
) =>
$$"""
[
{{string.Join(
",\n",
GetConstraints(tableAccessor, filterByAttr)
.Select(pair => $"{makeConstraintFn}({pair.Pos})")
)}}
]
""";
internal int? GetPrimaryKey(TableAccessor tableAccessor) =>
GetConstraints(tableAccessor, ColumnAttrs.PrimaryKey)
.Select(c => (int?)c.Pos)
.SingleOrDefault();
}
/// <summary>
/// Represents a view method declaration in a module.
/// </summary>
record ViewDeclaration
{
public readonly string Name;
public readonly string FullName;
public readonly bool IsAnonymous;
public readonly bool IsPublic;
public readonly TypeUse ReturnType;
public readonly EquatableArray<MemberDeclaration> Parameters;
public readonly Scope Scope;
public static uint ViewIndexCounter = 0;
public static uint GetNextViewIndex() => ViewIndexCounter++;
public static uint AnonViewIndexCounter = 0;
public static uint GetNextAnonViewIndex() => AnonViewIndexCounter++;
public ViewDeclaration(GeneratorAttributeSyntaxContext context, DiagReporter diag)
{
var methodSyntax = (MethodDeclarationSyntax)context.TargetNode;
var method = (IMethodSymbol)context.TargetSymbol;
var attr = context.Attributes.Single().ParseAs<ViewAttribute>();
var hasContextParam = method.Parameters.Length > 0;
var firstParamType = hasContextParam ? method.Parameters[0].Type : null;
var isAnonymousContext = firstParamType?.Name == "AnonymousViewContext";
var hasArguments = method.Parameters.Length > 1;
if (string.IsNullOrEmpty(attr.Name))
{
diag.Report(ErrorDescriptor.ViewMustHaveName, methodSyntax);
}
// TODO: Remove once Views support Private: Views must be Public currently
if (!attr.Public)
{
diag.Report(ErrorDescriptor.ViewMustBePublic, methodSyntax);
}
if (hasArguments)
{
diag.Report(ErrorDescriptor.ViewArgsUnsupported, methodSyntax);
}
Name = method.Name;
FullName = SymbolToName(method);
IsPublic = attr.Public;
IsAnonymous = isAnonymousContext;
ReturnType = TypeUse.Parse(method, method.ReturnType, diag);
Scope = new Scope(methodSyntax.Parent as MemberDeclarationSyntax);
if (method.Parameters.Length == 0)
{
diag.Report(ErrorDescriptor.ViewContextParam, methodSyntax);
}
else if (
method.Parameters[0].Type
is not INamedTypeSymbol { Name: "ViewContext" or "AnonymousViewContext" }
)
{
diag.Report(ErrorDescriptor.ViewContextParam, methodSyntax);
}
// Validate return type: must be Option<T> or Vec<T>
if (
!ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.ValueOption")
&& !ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.List")
)
{
diag.Report(ErrorDescriptor.ViewInvalidReturn, methodSyntax);
}
Parameters = new(
method
.Parameters.Skip(1)
.Select(p => new MemberDeclaration(p, p.Type, diag))
.ToImmutableArray()
);
}
public string GenerateViewDef(uint Index) =>
$$$"""
new global::SpacetimeDB.Internal.RawViewDefV9(
Name: "{{{Name}}}",
Index: {{{Index}}},
IsPublic: {{{IsPublic.ToString().ToLower()}}},
IsAnonymous: {{{IsAnonymous.ToString().ToLower()}}},
Params: [{{{MemberDeclaration.GenerateDefs(Parameters)}}}],
ReturnType: new {{{ReturnType.BSATNName}}}().GetAlgebraicType(registrar)
);
""";
public string GenerateDispatcherClass()
{
var paramReads = string.Join(
"\n ",
Parameters.Select(p =>
$"var {p.Name} = {p.Name}{TypeUse.BsatnFieldSuffix}.Read(reader);"
)
);
var makeViewDefMethod = IsAnonymous ? "MakeAnonymousViewDef" : "MakeViewDef";
var interfaceName = IsAnonymous
? "global::SpacetimeDB.Internal.IAnonymousView"
: "global::SpacetimeDB.Internal.IView";
var interfaceContext = IsAnonymous
? "global::SpacetimeDB.Internal.IAnonymousViewContext"
: "global::SpacetimeDB.Internal.IViewContext";
var concreteContext = IsAnonymous
? "SpacetimeDB.AnonymousViewContext"
: "SpacetimeDB.ViewContext";
var invocationArgs =
Parameters.Length == 0 ? "" : ", " + string.Join(", ", Parameters.Select(p => p.Name));
var index = IsAnonymous ? GetNextAnonViewIndex() : GetNextViewIndex();
return $$$"""
sealed class {{{Name}}}ViewDispatcher : {{{interfaceName}}} {
{{{MemberDeclaration.GenerateBsatnFields(Accessibility.Private, Parameters)}}}
private static readonly {{{ReturnType.BSATNName}}} returnRW = new();
public SpacetimeDB.Internal.RawViewDefV9 {{{makeViewDefMethod}}}(SpacetimeDB.BSATN.ITypeRegistrar registrar)
=> {{{GenerateViewDef(index)}}}
public byte[] Invoke(
System.IO.BinaryReader reader,
{{{interfaceContext}}} ctx
) {
try {
{{{paramReads}}}
var returnValue = {{{FullName}}}(({{{concreteContext}}})ctx{{{invocationArgs}}});
using var output = new System.IO.MemoryStream();
using var writer = new System.IO.BinaryWriter(output);
returnRW.Write(writer, returnValue);
return output.ToArray();
} catch (System.Exception e) {
global::SpacetimeDB.Log.Error("Error in view '{{{Name}}}': " + e);
throw;
}
}
}
""";
}
public string GenerateClass()
{
var builder = new Scope.Extensions(Scope, FullName);
builder.Contents.Append(GenerateDispatcherClass());
return builder.ToString();
}
}
/// <summary>
/// Represents a reducer method declaration in a module.
/// </summary>
record ReducerDeclaration
{
public readonly string Name;
public readonly ReducerKind Kind;
public readonly string FullName;
public readonly EquatableArray<MemberDeclaration> Args;
public readonly Scope Scope;
private readonly bool HasWrongSignature;
public ReducerDeclaration(GeneratorAttributeSyntaxContext context, DiagReporter diag)
{
var methodSyntax = (MethodDeclarationSyntax)context.TargetNode;
var method = (IMethodSymbol)context.TargetSymbol;
var attr = context.Attributes.Single().ParseAs<ReducerAttribute>();
if (!method.ReturnsVoid)
{
diag.Report(ErrorDescriptor.ReducerReturnType, methodSyntax);
}
if (
method.Parameters.FirstOrDefault()?.Type
is not INamedTypeSymbol { Name: "ReducerContext" }
)
{
diag.Report(ErrorDescriptor.ReducerContextParam, methodSyntax);
HasWrongSignature = true;
}
Name = method.Name;
if (Name.Length >= 2)
{
var prefix = Name[..2];
if (prefix is "__" or "on" or "On")
{
diag.Report(ErrorDescriptor.ReducerReservedPrefix, (methodSyntax, prefix));
}
}
Kind = attr.Kind;
FullName = SymbolToName(method);
Args = new(
method
.Parameters.Skip(1)
.Select(p => new MemberDeclaration(p, p.Type, diag))
.ToImmutableArray()
);
Scope = new Scope(methodSyntax.Parent as MemberDeclarationSyntax);
}
public string GenerateClass()
{
var invocation = HasWrongSignature
? "throw new System.InvalidOperationException()"
: $"{FullName}({string.Join(
", ",
Args.Select(a => $"{a.Name}{TypeUse.BsatnFieldSuffix}.Read(reader)").Prepend("(SpacetimeDB.ReducerContext)ctx")
)})";
return $$"""
class {{Name}}: SpacetimeDB.Internal.IReducer {
{{MemberDeclaration.GenerateBsatnFields(Accessibility.Private, Args)}}
public SpacetimeDB.Internal.RawReducerDefV9 MakeReducerDef(SpacetimeDB.BSATN.ITypeRegistrar registrar) => new (
nameof({{Name}}),
[{{MemberDeclaration.GenerateDefs(Args)}}],
{{Kind switch
{
ReducerKind.Init => "SpacetimeDB.Internal.Lifecycle.Init",
ReducerKind.ClientConnected => "SpacetimeDB.Internal.Lifecycle.OnConnect",
ReducerKind.ClientDisconnected => "SpacetimeDB.Internal.Lifecycle.OnDisconnect",
_ => "null"
}}}
);
public void Invoke(BinaryReader reader, SpacetimeDB.Internal.IReducerContext ctx) {
{{invocation}};
}
}
""";
}
public Scope.Extensions GenerateSchedule()
{
var extensions = new Scope.Extensions(Scope, FullName);
// Mark the API as unstable. We use name `STDB_UNSTABLE` because:
// 1. It's a close equivalent of the `unstable` Cargo feature in Rust.
// 2. Our diagnostic IDs use either BSATN or STDB prefix depending on the package.
// 3. We don't expect to mark individual experimental features with numeric IDs, so we don't use the standard 1234 suffix.
extensions.Contents.Append(
$$"""
[System.Diagnostics.CodeAnalysis.Experimental("STDB_UNSTABLE")]
public static void VolatileNonatomicScheduleImmediate{{Name}}({{string.Join(
", ",
Args.Select(a => $"{a.Type.Name} {a.Name}")
)}}) {
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream);
{{string.Join(
"\n",
Args.Select(a => $"new {a.Type.BSATNName}().Write(writer, {a.Name});")
)}}
SpacetimeDB.Internal.IReducer.VolatileNonatomicScheduleImmediate(nameof({{Name}}), stream);
}
"""
);
return extensions;
}
}
record ClientVisibilityFilterDeclaration
{
public readonly string FullName;
public string GlobalName => $"global::{FullName}";
public ClientVisibilityFilterDeclaration(
GeneratorAttributeSyntaxContext context,
DiagReporter diag
)
{
var fieldSymbol = (IFieldSymbol)context.TargetSymbol;
if (
!fieldSymbol.IsStatic
|| !fieldSymbol.IsReadOnly
|| fieldSymbol.DeclaredAccessibility != Accessibility.Public
)
{
diag.Report(ErrorDescriptor.ClientVisibilityNotPublicStaticReadonly, fieldSymbol);
}
if (fieldSymbol.Type.ToString() is not "SpacetimeDB.Filter")
{
diag.Report(ErrorDescriptor.ClientVisibilityNotFilter, fieldSymbol);
}
FullName = SymbolToName(fieldSymbol);
}
}
[Generator]
public class Module : IIncrementalGenerator
{
/// <summary>
/// Collects distinct items from a source sequence, ensuring no duplicate export names exist.
/// </summary>
/// <typeparam name="T">The type of items being collected</typeparam>
/// <param name="kind">The category/type of items being collected (used for error messages)</param>
/// <param name="context">The incremental generator context for reporting diagnostics</param>
/// <param name="source">The source sequence of items to process</param>
/// <param name="toExportName">Function to get the export name for an item (used for deduplication)</param>
/// <param name="toFullName">Function to get the full name of an item (used for error messages)</param>
/// <returns>An incremental value provider containing the distinct items</returns>
private static IncrementalValueProvider<EquatableArray<T>> CollectDistinct<T>(
string kind,
IncrementalGeneratorInitializationContext context,
IncrementalValuesProvider<T> source,
Func<T, string> toExportName,
Func<T, string> toFullName
)
where T : IEquatable<T>
{
var results = source
.Collect()
.Select(
(collected, ct) =>
DiagReporter.With(
Location.None,
diag =>
{
var grouped = collected
.GroupBy(toExportName)
// Sort tables and reducers by name to match Rust behaviour.
// Not really important outside of testing, but for testing
// it matters because we commit module-bindings
// so they need to match 1:1 between different langs.
.OrderBy(g => g.Key);
foreach (var group in grouped.Where(group => group.Count() > 1))
{
diag.Report(
ErrorDescriptor.DuplicateExport,
(kind, group.Key, group.Select(toFullName))
);
}
return new EquatableArray<T>(
// Only return first item from each group.
// We already reported duplicates ourselves, and don't want MSBuild to produce lots of duplicate errors too.
grouped.Select(Enumerable.First).ToImmutableArray()
);
}
)
);
context.RegisterSourceOutput(
results,
(context, results) =>
{
foreach (var result in results.Diag)
{
context.ReportDiagnostic(result);
}
}
);
return results
.Select((result, ct) => result.Parsed)
.WithTrackingName($"SpacetimeDB.{kind}.Collect");
}
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var tables = context
.SyntaxProvider.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: typeof(TableAttribute).FullName,
predicate: (node, ct) => true, // already covered by attribute restrictions
transform: (context, ct) =>
context.ParseWithDiags(diag => new TableDeclaration(context, diag))
)
.ReportDiagnostics(context)
.WithTrackingName("SpacetimeDB.Table.Parse");
tables
.Select((t, ct) => t.ToExtensions())
.WithTrackingName("SpacetimeDB.Table.GenerateExtensions")
.RegisterSourceOutputs(context);
var viewDeclarations = context
.SyntaxProvider.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: typeof(ViewAttribute).FullName!,
predicate: (node, _) => node is MethodDeclarationSyntax,
transform: (ctx, _) => ctx.ParseWithDiags(diag => new ViewDeclaration(ctx, diag))
)
.ReportDiagnostics(context)
.WithTrackingName("SpacetimeDB.View.Parse");
var views = CollectDistinct(
"View",
context,
viewDeclarations,
v => v.Name,
v => v.FullName
);
var reducers = context
.SyntaxProvider.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: typeof(ReducerAttribute).FullName,
predicate: (node, ct) => true, // already covered by attribute restrictions
transform: (context, ct) =>
context.ParseWithDiags(diag => new ReducerDeclaration(context, diag))
)
.ReportDiagnostics(context)
.WithTrackingName("SpacetimeDB.Reducer.Parse");
reducers
.Select((r, ct) => r.GenerateSchedule())
.WithTrackingName("SpacetimeDB.Reducer.GenerateSchedule")
.RegisterSourceOutputs(context);
context.RegisterSourceOutput(
reducers
.Where(r => r.Kind != ReducerKind.UserDefined)
.Collect()
.SelectMany(
(reducers, ct) =>
reducers
.GroupBy(r => r.Kind)
.Where(group => group.Count() > 1)
.Select(group =>
ErrorDescriptor.DuplicateSpecialReducer.ToDiag(
(group.Key, group.Select(r => r.FullName))
)
)
),
(ctx, diag) => ctx.ReportDiagnostic(diag)
);
var addReducers = CollectDistinct(
"Reducer",
context,
reducers
.Select((r, ct) => (r.Name, r.FullName, Class: r.GenerateClass()))
.WithTrackingName("SpacetimeDB.Reducer.GenerateClass"),
r => r.Name,
r => r.FullName
);
var tableAccessors = CollectDistinct(
"Table",
context,
tables
.SelectMany((t, ct) => t.GenerateTableAccessors())
.WithTrackingName("SpacetimeDB.Table.GenerateTableAccessors"),
v => v.tableAccessorName,
v => v.tableName
);
var readOnlyAccessors = CollectDistinct(
"TableReadOnly",
context,
tables
.SelectMany((t, ct) => t.GenerateReadOnlyAccessors())
.WithTrackingName("SpacetimeDB.Table.GenerateReadOnlyAccessors"),
v => v.tableAccessorName + "ReadOnly",
v => v.tableName
);
var rlsFilters = context
.SyntaxProvider.ForAttributeWithMetadataName(
#pragma warning disable STDB_UNSTABLE
fullyQualifiedMetadataName: typeof(ClientVisibilityFilterAttribute).FullName,
#pragma warning restore STDB_UNSTABLE
predicate: (node, ct) => true,
transform: (context, ct) =>
context.ParseWithDiags(diag => new ClientVisibilityFilterDeclaration(
context,
diag
))
)
.ReportDiagnostics(context)
.WithTrackingName("SpacetimeDB.ClientVisibilityFilter.Parse");
var rlsFiltersArray = CollectDistinct(
"ClientVisibilityFilter",
context,
rlsFilters,
(f) => f.FullName,
(f) => f.FullName
);
var columnDefaultValues = CollectDistinct(
"ColumnDefaultValues",
context,
tables
.SelectMany((t, ct) => t.GenerateDefaultValues())
.WithTrackingName("SpacetimeDB.Table.GenerateDefaultValues"),
v => v.tableName + "_" + v.columnId,
v => v.tableName + "_" + v.columnId
);
// Register the generated source code with the compilation context as part of module publishing
// Once the compilation is complete, the generated code will be used to create tables and reducers in the database
context.RegisterSourceOutput(
tableAccessors
.Combine(addReducers)
.Combine(readOnlyAccessors)
.Combine(views)
.Combine(rlsFiltersArray)
.Combine(columnDefaultValues),
(context, tuple) =>
{
var (
((((tableAccessors, addReducers), readOnlyAccessors), views), rlsFilters),
columnDefaultValues
) = tuple;
// Don't generate the FFI boilerplate if there are no tables or reducers.
if (tableAccessors.Array.IsEmpty && addReducers.Array.IsEmpty)
{
return;
}
context.AddSource(
"FFI.cs",
$$"""
// <auto-generated />
#nullable enable
// The runtime already defines SpacetimeDB.Internal.LocalReadOnly in Runtime\Internal\Module.cs as an empty partial type.
// This is needed so every module build doesn't generate a full LocalReadOnly type, but just adds on to the existing.
// We extend it here with generated table accessors, and just need to suppress the duplicate-type warning.
#pragma warning disable CS0436
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SpacetimeDB {
public sealed record ReducerContext : DbContext<Local>, Internal.IReducerContext {
public readonly Identity Sender;
public readonly ConnectionId? ConnectionId;
public readonly Random Rng;
public readonly Timestamp Timestamp;
public readonly AuthCtx SenderAuth;
// We need this property to be non-static for parity with client SDK.
public Identity Identity => Internal.IReducerContext.GetIdentity();
internal ReducerContext(Identity identity, ConnectionId? connectionId, Random random, Timestamp time) {
Sender = identity;
ConnectionId = connectionId;
Rng = random;
Timestamp = time;
SenderAuth = AuthCtx.BuildFromSystemTables(connectionId, identity);
}
}
public sealed record ViewContext : DbContext<Internal.LocalReadOnly>, Internal.IViewContext
{
public Identity Sender { get; }
internal ViewContext(Identity sender, Internal.LocalReadOnly db)
: base(db)
{
Sender = sender;
}
}
public sealed record AnonymousViewContext : DbContext<Internal.LocalReadOnly>, Internal.IAnonymousViewContext
{
internal AnonymousViewContext(Internal.LocalReadOnly db)
: base(db) { }
}
namespace Internal.TableHandles {
{{string.Join("\n", tableAccessors.Select(v => v.tableAccessor))}}
}
public sealed class Local {
{{string.Join("\n", tableAccessors.Select(v => v.getter))}}
}
}
{{string.Join("\n", views.Array.Select(v => v.GenerateDispatcherClass()))}}
namespace SpacetimeDB.Internal.ViewHandles {
{{string.Join("\n", readOnlyAccessors.Array.Select(v => v.readOnlyAccessor))}}
}
namespace SpacetimeDB.Internal {
public sealed partial class LocalReadOnly {
{{string.Join("\n", readOnlyAccessors.Select(v => v.readOnlyGetter))}}
}
}
static class ModuleRegistration {
{{string.Join("\n", addReducers.Select(r => r.Class))}}
#if EXPERIMENTAL_WASM_AOT
// In AOT mode we're building a library.
// Main method won't be called automatically, so we need to export it as a preinit function.
[UnmanagedCallersOnly(EntryPoint = "__preinit__10_init_csharp")]
#else
// Prevent trimming of FFI exports that are invoked from C and not visible to C# trimmer.
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, typeof(SpacetimeDB.Internal.Module))]
#endif
public static void Main() {
SpacetimeDB.Internal.Module.SetReducerContextConstructor((identity, connectionId, random, time) => new SpacetimeDB.ReducerContext(identity, connectionId, random, time));
SpacetimeDB.Internal.Module.SetViewContextConstructor(identity => new SpacetimeDB.ViewContext(identity, new SpacetimeDB.Internal.LocalReadOnly()));
SpacetimeDB.Internal.Module.SetAnonymousViewContextConstructor(() => new SpacetimeDB.AnonymousViewContext(new SpacetimeDB.Internal.LocalReadOnly()));
var __memoryStream = new MemoryStream();
var __writer = new BinaryWriter(__memoryStream);
{{string.Join(
"\n",
addReducers.Select(r =>
$"SpacetimeDB.Internal.Module.RegisterReducer<{r.Name}>();"
)
)}}
{{string.Join(
"\n",
views.Array.Select(v =>
v.IsAnonymous
? $"SpacetimeDB.Internal.Module.RegisterAnonymousView<{v.Name}ViewDispatcher>();"
: $"SpacetimeDB.Internal.Module.RegisterView<{v.Name}ViewDispatcher>();"
)
)}}
{{string.Join(
"\n",
tableAccessors.Select(t => $"SpacetimeDB.Internal.Module.RegisterTable<{t.tableName}, SpacetimeDB.Internal.TableHandles.{t.tableAccessorName}>();")
)}}
{{string.Join(
"\n",
rlsFilters.Select(f => $"SpacetimeDB.Internal.Module.RegisterClientVisibilityFilter({f.GlobalName});")
)}}
{{string.Join(
"\n",
columnDefaultValues.Select(d =>
"{\n"
+$"var value = new {d.BSATNTypeName}();\n"
+"__memoryStream.Position = 0;\n"
+"__memoryStream.SetLength(0);\n"
+$"value.Write(__writer, {d.value});\n"
+"var array = __memoryStream.ToArray();\n"
+$"SpacetimeDB.Internal.Module.RegisterTableDefaultValue(\"{d.tableName}\", {d.columnId}, array);"
+ "\n}\n")
)}}
}
// Exports only work from the main assembly, so we need to generate forwarding methods.
#if EXPERIMENTAL_WASM_AOT
[UnmanagedCallersOnly(EntryPoint = "__describe_module__")]
public static void __describe_module__(SpacetimeDB.Internal.BytesSink d) => SpacetimeDB.Internal.Module.__describe_module__(d);
[UnmanagedCallersOnly(EntryPoint = "__call_reducer__")]
public static SpacetimeDB.Internal.Errno __call_reducer__(
uint id,
ulong sender_0,
ulong sender_1,
ulong sender_2,
ulong sender_3,
ulong conn_id_0,
ulong conn_id_1,
SpacetimeDB.Timestamp timestamp,
SpacetimeDB.Internal.BytesSource args,
SpacetimeDB.Internal.BytesSink error
) => SpacetimeDB.Internal.Module.__call_reducer__(
id,
sender_0,
sender_1,
sender_2,
sender_3,
conn_id_0,
conn_id_1,
timestamp,
args,
error
);
#endif
}
#pragma warning restore CS0436
"""
);
}
);
}
}