mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-03-20 09:01:05 +08:00
## Summary
This PR fixes an issue where C# reserved keywords (like `params`,
`class`, `event`, etc.) used as field or parameter names in SpacetimeDB
types would cause compilation errors in the generated code.
## Problem
When a user defines a table or reducer with a field named using a C#
reserved keyword:
```csharp
[SpacetimeDB.Table]
public partial struct MyTable
{
public int @params; // User escapes it in their code
public string @class;
}
```
The codegen would generate invalid C# like:
```csharp
// Generated code (broken)
public int params; // Error: keyword used as identifier
public void Read(BinaryReader reader) {
params = ...; // Error
}
```
## Solution
1. Added an `Identifier` property to `MemberDeclaration` in the codegen
that automatically detects C# reserved keywords using
`SyntaxFacts.GetKeywordKind()` and prefixes them with `@` when needed.
2. Updated all code generation sites to use `Identifier` instead of
`Name` when generating:
- Field declarations
- Property accessors
- Constructor parameters
- BSATN serialization code
- Index accessors
- Reducer/procedure parameters
3. Added a regression test that verifies tables, reducers, and
procedures with keyword field names compile successfully.
## Test Plan
- Added `CSharpKeywordIdentifiersAreEscapedInGeneratedCode` test that
creates a table with `@class` and `@params` fields, plus a reducer and
procedure with keyword parameters
- Existing tests continue to pass (verified locally with
`FormerlyForbiddenFieldNames` fixture which already tests edge cases
like `Read`, `Write`, `GetAlgebraicType`)
Fixes #4529
---------
Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com>
Co-authored-by: Ryan <r.ekhoff@clockworklabs.io>
2506 lines
103 KiB
C#
2506 lines
103 KiB
C#
namespace SpacetimeDB.Codegen;
|
|
|
|
using System;
|
|
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);
|
|
}
|
|
}
|
|
|
|
record SettingsDeclaration
|
|
{
|
|
public readonly string FullName;
|
|
public readonly string? CaseConversionPolicy;
|
|
|
|
private static readonly string[] CaseConversionPolicyTypeNames =
|
|
[
|
|
"SpacetimeDB.CaseConversionPolicy",
|
|
"SpacetimeDB.Internal.CaseConversionPolicy", // backward compat
|
|
];
|
|
|
|
public SettingsDeclaration(GeneratorAttributeSyntaxContext context, DiagReporter diag)
|
|
{
|
|
var fieldSymbol = (IFieldSymbol)context.TargetSymbol;
|
|
FullName = SymbolToName(fieldSymbol);
|
|
|
|
if (!fieldSymbol.IsConst)
|
|
{
|
|
diag.Report(ErrorDescriptor.SettingsMustBeConstCaseConversionPolicy, fieldSymbol);
|
|
return;
|
|
}
|
|
if (!CaseConversionPolicyTypeNames.Contains(fieldSymbol.Type.ToString()))
|
|
{
|
|
diag.Report(ErrorDescriptor.SettingsMustBeConstCaseConversionPolicy, fieldSymbol);
|
|
return;
|
|
}
|
|
if (fieldSymbol.ConstantValue is null)
|
|
{
|
|
diag.Report(ErrorDescriptor.SettingsMustBeConstCaseConversionPolicy, fieldSymbol);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var n = Convert.ToInt32(fieldSymbol.ConstantValue);
|
|
CaseConversionPolicy = n switch
|
|
{
|
|
0 => "None",
|
|
1 => "SnakeCase",
|
|
2 => "CamelCase",
|
|
3 => "PascalCase",
|
|
_ => null,
|
|
};
|
|
}
|
|
catch
|
|
{
|
|
CaseConversionPolicy = null;
|
|
}
|
|
|
|
if (CaseConversionPolicy is null)
|
|
{
|
|
diag.Report(ErrorDescriptor.SettingsMustBeConstCaseConversionPolicy, fieldSymbol);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <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"
|
|
or "SpacetimeDB.Uuid",
|
|
_ => 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({Identifier}), BSATN.{Identifier}{TypeUse.BsatnFieldSuffix}.GetAlgebraicType(registrar))";
|
|
}
|
|
|
|
record Scheduled(string ReducerName, int ScheduledAtColumn);
|
|
|
|
record TableAccessor
|
|
{
|
|
public readonly string Name;
|
|
public readonly string? CanonicalName;
|
|
public readonly bool IsPublic;
|
|
public readonly bool IsEvent;
|
|
public readonly Scheduled? Scheduled;
|
|
|
|
public string Identifier => EscapeIdentifier(Name);
|
|
|
|
public TableAccessor(TableDeclaration table, AttributeData data, DiagReporter diag)
|
|
{
|
|
var attr = data.ParseAs<TableAttribute>();
|
|
|
|
Name = attr.Accessor ?? table.ShortName;
|
|
CanonicalName = attr.Name;
|
|
IsPublic = attr.Public;
|
|
IsEvent = attr.Event;
|
|
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 string? CanonicalName;
|
|
public readonly TableIndexType Type;
|
|
|
|
public string AccessorIdentifier => EscapeIdentifier(AccessorName);
|
|
|
|
// 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="canonicalName">Explicit canonical name override for this index, if any.</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,
|
|
string? canonicalName,
|
|
ImmutableArray<ColumnRef> columns,
|
|
string? tableName,
|
|
TableIndexType type
|
|
)
|
|
{
|
|
Columns = new(columns);
|
|
Table = tableName;
|
|
var columnNames = string.Join("_", columns.Select(c => c.Name));
|
|
AccessorName = accessorName ?? columnNames;
|
|
CanonicalName = canonicalName;
|
|
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,
|
|
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(
|
|
global::SpacetimeDB.Index.BTreeAttribute attr,
|
|
ImmutableArray<ColumnRef> columns
|
|
)
|
|
: this(attr.Accessor, 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,
|
|
global::SpacetimeDB.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 (string.IsNullOrWhiteSpace(attr.Accessor))
|
|
{
|
|
diag.Report(ErrorDescriptor.TableLevelIndexMissingAccessor, data);
|
|
}
|
|
|
|
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<global::SpacetimeDB.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,
|
|
global::SpacetimeDB.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<global::SpacetimeDB.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(global::SpacetimeDB.Index.BTreeAttribute).FullName.Replace('+', '.');
|
|
|
|
public static bool CanParse(AttributeData data) =>
|
|
data.AttributeClass?.ToString() == BTreeAttrName;
|
|
|
|
public string GenerateIndexDef(TableAccessor tableAccessor) =>
|
|
$$"""
|
|
new(
|
|
SourceName: "{{StandardIndexName(tableAccessor)}}",
|
|
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;
|
|
|
|
private readonly bool isRowStruct;
|
|
|
|
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;
|
|
|
|
isRowStruct = ((INamedTypeSymbol)context.TargetSymbol).IsValueType;
|
|
|
|
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}";
|
|
|
|
var uniqueIndexBase = isRowStruct ? "UniqueIndex" : "RefUniqueIndex";
|
|
|
|
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);
|
|
var updateMethod = ct.Attr.HasFlag(ColumnAttrs.PrimaryKey)
|
|
? $"public {globalName} Update({globalName} row) => DoUpdate(row);"
|
|
: "";
|
|
yield return $$"""
|
|
{{vis}} sealed class {{f.Identifier}}UniqueIndex : {{uniqueIndexBase}}<{{tableAccessor.Identifier}}, {{globalName}}, {{f.Type.Name}}, {{f.Type.BSATNName}}> {
|
|
internal {{f.Identifier}}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) => FindSingle(key);
|
|
{{updateMethod}}
|
|
}
|
|
{{vis}} {{f.Identifier}}UniqueIndex {{f.Identifier}} => new();
|
|
""";
|
|
}
|
|
|
|
foreach (var index in GetIndexes(tableAccessor))
|
|
{
|
|
var name = index.AccessorName;
|
|
var identifierName = index.AccessorIdentifier;
|
|
|
|
// 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 {{identifierName}}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.Identifier}");
|
|
var lastScalar = $"{members[n].Type.Name} {members[n].Identifier}";
|
|
var lastBounds =
|
|
$"global::SpacetimeDB.Bound<{members[n].Type.Name}> {members[n].Identifier}";
|
|
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].Identifier;
|
|
}
|
|
|
|
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} {identifierName}Index {identifierName} => new();\n";
|
|
}
|
|
}
|
|
|
|
private IEnumerable<string> GenerateReadOnlyAccessorFilters(TableAccessor tableAccessor)
|
|
{
|
|
var vis = SyntaxFacts.GetText(Visibility);
|
|
var globalName = $"global::{FullName}";
|
|
|
|
var uniqueIndexBase = isRowStruct
|
|
? "global::SpacetimeDB.Internal.ReadOnlyUniqueIndex"
|
|
: "global::SpacetimeDB.Internal.ReadOnlyRefUniqueIndex";
|
|
|
|
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.Identifier}}}Index
|
|
: {{{uniqueIndexBase}}}<
|
|
global::SpacetimeDB.Internal.ViewHandles.{{{tableAccessor.Identifier}}}ReadOnly,
|
|
{{{globalName}}},
|
|
{{{f.Type.Name}}},
|
|
{{{f.Type.BSATNName}}}>
|
|
{
|
|
internal {{{f.Identifier}}}Index() : base("{{{standardIndexName}}}") { }
|
|
|
|
public {{{globalName}}}? Find({{{f.Type.Name}}} key) => FindSingle(key);
|
|
}
|
|
|
|
public {{{f.Identifier}}}Index {{{f.Identifier}}} => 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 identifierName = index.AccessorIdentifier;
|
|
|
|
var blocks = new List<string>
|
|
{
|
|
$$$"""
|
|
public sealed class {{{identifierName}}}Index
|
|
: global::SpacetimeDB.Internal.ReadOnlyIndexBase<{{{globalName}}}>
|
|
{
|
|
internal {{{identifierName}}}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.Identifier}")
|
|
);
|
|
var boundsArgs = string.Join(
|
|
", ",
|
|
declaringMembers
|
|
.Take(n)
|
|
.Select(m => $"{m.Type.Name} {m.Identifier}")
|
|
.Append(
|
|
$"global::SpacetimeDB.Bound<{declaringMembers[^1].Type.Name}> {declaringMembers[^1].Identifier}"
|
|
)
|
|
);
|
|
|
|
var ctorArg = n == 0 ? declaringMembers[0].Identifier : "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} {identifierName}Index {identifierName} => 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 accessorIdentifier = v.Identifier;
|
|
var iTable =
|
|
$"global::SpacetimeDB.Internal.ITableView<{accessorIdentifier}, {globalName}>";
|
|
yield return new(
|
|
v.Name,
|
|
globalName,
|
|
$$$"""
|
|
{{{SyntaxFacts.GetText(Visibility)}}} readonly struct {{{accessorIdentifier}}} : {{{iTable}}} {
|
|
public static {{{globalName}}} ReadGenFields(System.IO.BinaryReader reader, {{{globalName}}} row) {
|
|
{{{string.Join(
|
|
"\n",
|
|
autoIncFields.Select(m =>
|
|
$$"""
|
|
if (row.{{m.Identifier}} == default)
|
|
{
|
|
row.{{m.Identifier}} = {{globalName}}.BSATN.{{m.Identifier}}{{TypeUse.BsatnFieldSuffix}}.Read(reader);
|
|
}
|
|
"""
|
|
)
|
|
)}}}
|
|
return row;
|
|
}
|
|
|
|
public static SpacetimeDB.Internal.RawTableDefV10 MakeTableDesc(SpacetimeDB.BSATN.ITypeRegistrar registrar) => new (
|
|
SourceName: nameof({{{accessorIdentifier}}}),
|
|
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(v))
|
|
)}}}
|
|
],
|
|
Constraints: {{{GenConstraintList(v, ColumnAttrs.Unique, $"{iTable}.MakeUniqueConstraint")}}},
|
|
Sequences: {{{GenConstraintList(v, ColumnAttrs.AutoInc, $"{iTable}.MakeSequence")}}},
|
|
TableType: SpacetimeDB.Internal.TableType.User,
|
|
TableAccess: SpacetimeDB.Internal.TableAccess.{{{(v.IsPublic ? "Public" : "Private")}}},
|
|
DefaultValues: [],
|
|
IsEvent: {{{(v.IsEvent ? "true" : "false")}}}
|
|
);
|
|
|
|
public static SpacetimeDB.Internal.RawScheduleDefV10? MakeScheduleDesc() => {{{(
|
|
v.Scheduled is { } scheduled
|
|
? $"{iTable}.MakeSchedule(\"{scheduled.ReducerName}\", {scheduled.ScheduledAtColumn})"
|
|
: "null"
|
|
)}}};
|
|
|
|
/// <summary>
|
|
/// Returns the number of rows in this table.
|
|
///
|
|
/// This reads datastore metadata, so it runs in constant time.
|
|
/// It also takes into account modifications by the current transaction.
|
|
/// </summary>
|
|
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.{accessorIdentifier} {accessorIdentifier} => 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 accessorIdentifier = accessor.Identifier;
|
|
|
|
var readOnlyIndexDecls = string.Join("\n", GenerateReadOnlyAccessorFilters(accessor));
|
|
var visibility = SyntaxFacts.GetText(Visibility);
|
|
yield return new(
|
|
accessor.Name,
|
|
globalName,
|
|
$$$"""
|
|
{{{visibility}}} sealed class {{{accessorIdentifier}}}ReadOnly
|
|
: global::SpacetimeDB.Internal.ReadOnlyTableView<{{{globalName}}}>
|
|
{
|
|
internal {{{accessorIdentifier}}}ReadOnly() : base("{{{accessor.Name}}}") { }
|
|
|
|
/// <summary>
|
|
/// Returns the number of rows in this table.
|
|
///
|
|
/// This reads datastore metadata, so it runs in constant time.
|
|
/// It also takes into account modifications by the current transaction.
|
|
/// </summary>
|
|
public ulong Count => DoCount();
|
|
|
|
{{{readOnlyIndexDecls}}}
|
|
}
|
|
""",
|
|
$"{visibility} global::SpacetimeDB.Internal.ViewHandles.{accessorIdentifier}ReadOnly {accessorIdentifier} => new();"
|
|
);
|
|
}
|
|
}
|
|
|
|
public IEnumerable<string> GenerateQueryBuilderMembers()
|
|
{
|
|
if (Kind is TypeKind.Sum)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
var vis = SyntaxFacts.GetText(Visibility);
|
|
var globalRowName = $"global::{FullName}";
|
|
|
|
foreach (var accessor in TableAccessors)
|
|
{
|
|
var accessorIdentifier = accessor.Identifier;
|
|
var tableName = accessor.Name;
|
|
var colsTypeName = $"{accessorIdentifier}Cols";
|
|
var ixColsTypeName = $"{accessorIdentifier}IxCols";
|
|
|
|
string ColDecl(ColumnDeclaration col)
|
|
{
|
|
var typeName = col.Type.Name;
|
|
var isNullable = typeName.EndsWith("?", StringComparison.Ordinal);
|
|
var valueTypeName = isNullable ? typeName[..^1] : typeName;
|
|
var colType = isNullable ? "global::SpacetimeDB.Col" : "global::SpacetimeDB.Col";
|
|
return $"public readonly {colType}<{globalRowName}, {valueTypeName}> {col.Identifier};";
|
|
}
|
|
|
|
string ColInit(ColumnDeclaration col)
|
|
{
|
|
var typeName = col.Type.Name;
|
|
var isNullable = typeName.EndsWith("?", StringComparison.Ordinal);
|
|
var valueTypeName = isNullable ? typeName[..^1] : typeName;
|
|
var colType = isNullable ? "global::SpacetimeDB.Col" : "global::SpacetimeDB.Col";
|
|
return $"{col.Identifier} = new {colType}<{globalRowName}, {valueTypeName}>(tableName, \"{col.Name}\");";
|
|
}
|
|
|
|
var colsDecls = string.Join("\n ", Members.Select(ColDecl));
|
|
var colsInits = string.Join("\n ", Members.Select(ColInit));
|
|
|
|
var ixPositions = new global::System.Collections.Generic.HashSet<int>();
|
|
foreach (var c in GetConstraints(accessor, ColumnAttrs.PrimaryKey | ColumnAttrs.Unique))
|
|
{
|
|
ixPositions.Add(c.Pos);
|
|
}
|
|
|
|
foreach (var ix in GetIndexes(accessor))
|
|
{
|
|
foreach (var colRef in ix.Columns.Array)
|
|
{
|
|
ixPositions.Add(colRef.Index);
|
|
}
|
|
}
|
|
|
|
var ixMembers = Members
|
|
.Select((m, i) => (m, i))
|
|
.Where(pair => ixPositions.Contains(pair.i))
|
|
.Select(pair => pair.m)
|
|
.ToArray();
|
|
|
|
string IxColDecl(ColumnDeclaration col)
|
|
{
|
|
var typeName = col.Type.Name;
|
|
var isNullable = typeName.EndsWith("?", StringComparison.Ordinal);
|
|
var valueTypeName = isNullable ? typeName[..^1] : typeName;
|
|
var colType = isNullable
|
|
? "global::SpacetimeDB.IxCol"
|
|
: "global::SpacetimeDB.IxCol";
|
|
return $"public readonly {colType}<{globalRowName}, {valueTypeName}> {col.Identifier};";
|
|
}
|
|
|
|
string IxColInit(ColumnDeclaration col)
|
|
{
|
|
var typeName = col.Type.Name;
|
|
var isNullable = typeName.EndsWith("?", StringComparison.Ordinal);
|
|
var valueTypeName = isNullable ? typeName[..^1] : typeName;
|
|
var colType = isNullable
|
|
? "global::SpacetimeDB.IxCol"
|
|
: "global::SpacetimeDB.IxCol";
|
|
return $"{col.Identifier} = new {colType}<{globalRowName}, {valueTypeName}>(tableName, \"{col.Name}\");";
|
|
}
|
|
|
|
var ixColsDecls = string.Join("\n ", ixMembers.Select(IxColDecl));
|
|
var ixColsInits = string.Join("\n ", ixMembers.Select(IxColInit));
|
|
|
|
yield return $$"""
|
|
{{vis}} readonly struct {{colsTypeName}}
|
|
{
|
|
{{colsDecls}}
|
|
|
|
internal {{colsTypeName}}(string tableName)
|
|
{
|
|
{{colsInits}}
|
|
}
|
|
}
|
|
|
|
{{vis}} readonly struct {{ixColsTypeName}}
|
|
{
|
|
{{ixColsDecls}}
|
|
|
|
internal {{ixColsTypeName}}(string tableName)
|
|
{
|
|
{{ixColsInits}}
|
|
}
|
|
}
|
|
|
|
public readonly partial struct QueryBuilder
|
|
{
|
|
{{vis}} global::SpacetimeDB.Table<{{globalRowName}}, {{colsTypeName}}, {{ixColsTypeName}}> {{accessorIdentifier}}() =>
|
|
new("{{tableName}}", new {{colsTypeName}}("{{tableName}}"), new {{ixColsTypeName}}("{{tableName}}"));
|
|
}
|
|
""";
|
|
}
|
|
}
|
|
|
|
/// <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? CanonicalName;
|
|
public readonly string FullName;
|
|
public readonly bool IsAnonymous;
|
|
public readonly bool IsPublic;
|
|
public readonly bool ReturnsQuery;
|
|
public readonly TypeUse ReturnType;
|
|
public readonly TypeUse? QueryRowType;
|
|
public readonly EquatableArray<MemberDeclaration> Parameters;
|
|
public readonly Scope Scope;
|
|
|
|
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.Accessor))
|
|
{
|
|
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 = attr.Accessor ?? method.Name;
|
|
CanonicalName = attr.Name;
|
|
FullName = SymbolToName(method);
|
|
IsPublic = attr.Public;
|
|
IsAnonymous = isAnonymousContext;
|
|
|
|
ReturnsQuery = false;
|
|
INamedTypeSymbol? iquery = null;
|
|
if (
|
|
method.ReturnType is INamedTypeSymbol
|
|
{
|
|
Name: "IQuery",
|
|
ContainingNamespace: { Name: "SpacetimeDB" },
|
|
TypeArguments: [var _]
|
|
} directIQuery
|
|
)
|
|
{
|
|
iquery = directIQuery;
|
|
}
|
|
else
|
|
{
|
|
iquery = method
|
|
.ReturnType.AllInterfaces.OfType<INamedTypeSymbol>()
|
|
.FirstOrDefault(i =>
|
|
i
|
|
is {
|
|
Name: "IQuery",
|
|
ContainingNamespace: { Name: "SpacetimeDB" },
|
|
TypeArguments.Length: 1
|
|
}
|
|
);
|
|
}
|
|
|
|
if (iquery is { TypeArguments: [var queryRowType] })
|
|
{
|
|
ReturnsQuery = true;
|
|
var rowType = TypeUse.Parse(method, queryRowType, diag);
|
|
QueryRowType = rowType;
|
|
ReturnType = rowType;
|
|
}
|
|
else
|
|
{
|
|
QueryRowType = null;
|
|
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 List<T>, T?, or IQuery<T>.
|
|
if (
|
|
!ReturnsQuery
|
|
&& !ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.ValueOption")
|
|
&& !ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.RefOption")
|
|
&& !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)
|
|
{
|
|
var returnTypeExpr = ReturnsQuery
|
|
? $"global::SpacetimeDB.BSATN.AlgebraicType.MakeQueryBuilderProductType(new {QueryRowType!.BSATNName}().GetAlgebraicType(registrar))"
|
|
: $"new {ReturnType.BSATNName}().GetAlgebraicType(registrar)";
|
|
return $$$"""
|
|
new global::SpacetimeDB.Internal.RawViewDefV10(
|
|
SourceName: "{{{Name}}}",
|
|
Index: {{{Index}}},
|
|
IsPublic: {{{IsPublic.ToString().ToLower()}}},
|
|
IsAnonymous: {{{IsAnonymous.ToString().ToLower()}}},
|
|
Params: [{{{MemberDeclaration.GenerateDefs(Parameters)}}}],
|
|
ReturnType: {{{returnTypeExpr}}}
|
|
);
|
|
""";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates the class responsible for evaluating a view.
|
|
/// If this is an anonymous view, the index corresponds to the position of this dispatcher in the `viewDispatchers` list of `RegisterView`.
|
|
/// Otherwise it corresponds to the position of this dispatcher in the `anonymousViewDispatchers` list of `RegisterAnonymousView`.
|
|
/// </summary>
|
|
public string GenerateDispatcherClass(uint index)
|
|
{
|
|
var paramReads = string.Join(
|
|
"\n ",
|
|
Parameters.Select(p =>
|
|
$"var {p.Identifier} = {p.Identifier}{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 isOption =
|
|
ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.ValueOption")
|
|
|| ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.RefOption");
|
|
|
|
var writeOutput =
|
|
ReturnsQuery
|
|
? $$$"""
|
|
var header = new global::SpacetimeDB.Internal.ViewResultHeader.RawSql(returnValue.ToSql());
|
|
var headerRW = new global::SpacetimeDB.Internal.ViewResultHeader.BSATN();
|
|
using var output = new System.IO.MemoryStream();
|
|
using var writer = new System.IO.BinaryWriter(output);
|
|
headerRW.Write(writer, header);
|
|
return output.ToArray();
|
|
"""
|
|
: isOption
|
|
? $$$"""
|
|
var listSerializer = {{{ReturnType.BSATNName}}}.GetListSerializer();
|
|
var listValue = ModuleRegistration.ToListOrEmpty(returnValue);
|
|
var header = new global::SpacetimeDB.Internal.ViewResultHeader.RowData(default);
|
|
var headerRW = new global::SpacetimeDB.Internal.ViewResultHeader.BSATN();
|
|
using var output = new System.IO.MemoryStream();
|
|
using var writer = new System.IO.BinaryWriter(output);
|
|
headerRW.Write(writer, header);
|
|
listSerializer.Write(writer, listValue);
|
|
return output.ToArray();
|
|
"""
|
|
: $$$"""
|
|
{{{ReturnType.BSATNName}}} returnRW = new();
|
|
var header = new global::SpacetimeDB.Internal.ViewResultHeader.RowData(default);
|
|
var headerRW = new global::SpacetimeDB.Internal.ViewResultHeader.BSATN();
|
|
using var output = new System.IO.MemoryStream();
|
|
using var writer = new System.IO.BinaryWriter(output);
|
|
headerRW.Write(writer, header);
|
|
returnRW.Write(writer, returnValue);
|
|
return output.ToArray();
|
|
""";
|
|
|
|
var invocationArgs =
|
|
Parameters.Length == 0
|
|
? ""
|
|
: ", " + string.Join(", ", Parameters.Select(p => p.Identifier));
|
|
return $$$"""
|
|
sealed class {{{Name}}}ViewDispatcher : {{{interfaceName}}} {
|
|
{{{MemberDeclaration.GenerateBsatnFields(Accessibility.Private, Parameters)}}}
|
|
|
|
public SpacetimeDB.Internal.RawViewDefV10 {{{makeViewDefMethod}}}(SpacetimeDB.BSATN.ITypeRegistrar registrar)
|
|
=> {{{GenerateViewDef(index)}}}
|
|
|
|
public byte[] Invoke(
|
|
System.IO.BinaryReader reader,
|
|
{{{interfaceContext}}} ctx
|
|
) {
|
|
try {
|
|
{{{paramReads}}}
|
|
var returnValue = {{{FullName}}}(({{{concreteContext}}})ctx{{{invocationArgs}}});
|
|
{{{writeOutput}}}
|
|
} catch (System.Exception e) {
|
|
global::SpacetimeDB.Log.Error("Error in view '{{{Name}}}': " + e);
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
""";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a reducer method declaration in a module.
|
|
/// </summary>
|
|
record ReducerDeclaration
|
|
{
|
|
public readonly string Name;
|
|
public readonly string? CanonicalName;
|
|
public readonly ReducerKind Kind;
|
|
public readonly string FullName;
|
|
public readonly EquatableArray<MemberDeclaration> Args;
|
|
public readonly Scope Scope;
|
|
private readonly bool HasWrongSignature;
|
|
|
|
public string Identifier => EscapeIdentifier(Name);
|
|
|
|
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;
|
|
CanonicalName = attr.Name;
|
|
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.Identifier}{TypeUse.BsatnFieldSuffix}.Read(reader)")
|
|
.Prepend("(SpacetimeDB.ReducerContext)ctx")
|
|
)})";
|
|
|
|
return $$"""
|
|
class {{Identifier}}: SpacetimeDB.Internal.IReducer {
|
|
{{MemberDeclaration.GenerateBsatnFields(Accessibility.Private, Args)}}
|
|
|
|
public SpacetimeDB.Internal.RawReducerDefV10 MakeReducerDef(SpacetimeDB.BSATN.ITypeRegistrar registrar) => new (
|
|
SourceName: nameof({{Identifier}}),
|
|
Params: [{{MemberDeclaration.GenerateDefs(Args)}}],
|
|
Visibility: SpacetimeDB.Internal.FunctionVisibility.ClientCallable,
|
|
OkReturnType: SpacetimeDB.BSATN.AlgebraicType.Unit,
|
|
ErrReturnType: new SpacetimeDB.BSATN.AlgebraicType.String(default)
|
|
);
|
|
|
|
public SpacetimeDB.Internal.Lifecycle? Lifecycle => {{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.Identifier}")
|
|
)}}) {
|
|
using var stream = new MemoryStream();
|
|
using var writer = new BinaryWriter(stream);
|
|
{{string.Join(
|
|
"\n",
|
|
Args.Select(a => $"new {a.Type.ToBSATNString()}().Write(writer, {a.Identifier});")
|
|
)}}
|
|
SpacetimeDB.Internal.IReducer.VolatileNonatomicScheduleImmediate(nameof({{Identifier}}), stream);
|
|
}
|
|
"""
|
|
);
|
|
|
|
return extensions;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a procedure method declaration in a module.
|
|
/// </summary>
|
|
record ProcedureDeclaration
|
|
{
|
|
public readonly string Name;
|
|
public readonly string? CanonicalName;
|
|
public readonly string FullName;
|
|
public readonly EquatableArray<MemberDeclaration> Args;
|
|
public readonly Scope Scope;
|
|
private readonly bool HasWrongSignature;
|
|
public readonly TypeUse ReturnType;
|
|
private readonly bool HasTxWrapper;
|
|
private readonly TypeUse? TxPayloadType;
|
|
private readonly bool TxPayloadIsUnit;
|
|
|
|
public string Identifier => EscapeIdentifier(Name);
|
|
|
|
public ProcedureDeclaration(GeneratorAttributeSyntaxContext context, DiagReporter diag)
|
|
{
|
|
var methodSyntax = (MethodDeclarationSyntax)context.TargetNode;
|
|
var method = (IMethodSymbol)context.TargetSymbol;
|
|
var attr = context.Attributes.Single().ParseAs<ProcedureAttribute>();
|
|
|
|
if (
|
|
method.Parameters.FirstOrDefault()?.Type
|
|
is not INamedTypeSymbol { Name: "ProcedureContext" }
|
|
)
|
|
{
|
|
diag.Report(ErrorDescriptor.ProcedureContextParam, methodSyntax);
|
|
HasWrongSignature = true;
|
|
}
|
|
|
|
Name = method.Name;
|
|
if (Name.Length >= 2)
|
|
{
|
|
var prefix = Name[..2];
|
|
if (prefix is "__" or "on" or "On")
|
|
{
|
|
diag.Report(ErrorDescriptor.ProcedureReservedPrefix, (methodSyntax, prefix));
|
|
}
|
|
}
|
|
|
|
ReturnType = TypeUse.Parse(method, method.ReturnType, diag);
|
|
|
|
if (
|
|
method.ReturnType
|
|
is INamedTypeSymbol
|
|
{
|
|
Name: "TxOutcome",
|
|
ContainingType: { Name: "ProcedureContext" }
|
|
} txOutcome
|
|
&& txOutcome.TypeArguments.Length == 1
|
|
)
|
|
{
|
|
HasTxWrapper = true;
|
|
TxPayloadType = TypeUse.Parse(method, txOutcome.TypeArguments[0], diag);
|
|
TxPayloadIsUnit = TxPayloadType.BSATNName == "SpacetimeDB.BSATN.Unit";
|
|
}
|
|
else if (
|
|
method.ReturnType
|
|
is INamedTypeSymbol
|
|
{
|
|
Name: "TxResult",
|
|
ContainingType: { Name: "ProcedureContext" }
|
|
} txResult
|
|
&& txResult.TypeArguments.Length == 2
|
|
)
|
|
{
|
|
HasTxWrapper = true;
|
|
TxPayloadType = TypeUse.Parse(method, txResult.TypeArguments[0], diag);
|
|
TxPayloadIsUnit = TxPayloadType.BSATNName == "SpacetimeDB.BSATN.Unit";
|
|
}
|
|
|
|
CanonicalName = attr.Name;
|
|
|
|
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 invocationArgs =
|
|
Args.Length == 0 ? "" : ", " + string.Join(", ", Args.Select(a => a.Identifier));
|
|
var invocation = $"{FullName}((SpacetimeDB.ProcedureContext)ctx{invocationArgs})";
|
|
|
|
var txPayload = TxPayloadType ?? ReturnType;
|
|
var txPayloadIsUnit = TxPayloadIsUnit;
|
|
|
|
string[] bodyLines;
|
|
|
|
if (HasWrongSignature)
|
|
{
|
|
bodyLines = new[]
|
|
{
|
|
"throw new System.InvalidOperationException(\"Invalid procedure signature.\");",
|
|
};
|
|
}
|
|
else if (HasTxWrapper)
|
|
{
|
|
var successLines = txPayloadIsUnit
|
|
? new[] { "return System.Array.Empty<byte>();" }
|
|
: new[]
|
|
{
|
|
"using var output = new MemoryStream();",
|
|
"using var writer = new BinaryWriter(output);",
|
|
"__txReturnRW.Write(writer, outcome.Value!);",
|
|
"return output.ToArray();",
|
|
};
|
|
|
|
bodyLines = new[]
|
|
{
|
|
$"var outcome = {invocation};",
|
|
"if (!outcome.IsSuccess)",
|
|
"{",
|
|
" throw outcome.Error ?? new System.InvalidOperationException(\"Transaction failed.\");",
|
|
"}",
|
|
}
|
|
.Concat(successLines)
|
|
.ToArray();
|
|
}
|
|
else if (ReturnType.Name == "SpacetimeDB.Unit")
|
|
{
|
|
bodyLines = new[] { $"{invocation};", "return System.Array.Empty<byte>();" };
|
|
}
|
|
else
|
|
{
|
|
var serializer = $"new {ReturnType.ToBSATNString()}()";
|
|
bodyLines = new[]
|
|
{
|
|
$"var result = {invocation};",
|
|
"using var output = new MemoryStream();",
|
|
"using var writer = new BinaryWriter(output);",
|
|
$"{serializer}.Write(writer, result);",
|
|
"return output.ToArray();",
|
|
};
|
|
}
|
|
|
|
var invokeBody = string.Join("\n", bodyLines.Select(line => $" {line}"));
|
|
var paramReads =
|
|
Args.Length == 0
|
|
? string.Empty
|
|
: string.Join(
|
|
"\n",
|
|
Args.Select(a =>
|
|
$" var {a.Identifier} = {a.Identifier}{TypeUse.BsatnFieldSuffix}.Read(reader);"
|
|
)
|
|
) + "\n";
|
|
|
|
var returnTypeExpr = HasTxWrapper
|
|
? (
|
|
txPayloadIsUnit
|
|
? "SpacetimeDB.BSATN.AlgebraicType.Unit"
|
|
: $"new {txPayload.ToBSATNString2()}().GetAlgebraicType(registrar)"
|
|
)
|
|
: (
|
|
ReturnType.Name == "SpacetimeDB.Unit"
|
|
? "SpacetimeDB.BSATN.AlgebraicType.Unit"
|
|
: $"new {ReturnType.ToBSATNString2()}().GetAlgebraicType(registrar)"
|
|
);
|
|
|
|
var classFields = MemberDeclaration.GenerateBsatnFields(Accessibility.Private, Args);
|
|
if (HasTxWrapper && !txPayloadIsUnit)
|
|
{
|
|
classFields +=
|
|
$"\n private {txPayload.BSATNName} __txReturnRW = new {txPayload.BSATNName}();";
|
|
}
|
|
|
|
return $$$"""
|
|
class {{{Identifier}}} : SpacetimeDB.Internal.IProcedure {
|
|
{{{classFields}}}
|
|
|
|
public SpacetimeDB.Internal.RawProcedureDefV10 MakeProcedureDef(SpacetimeDB.BSATN.ITypeRegistrar registrar) => new(
|
|
SourceName: nameof({{{Identifier}}}),
|
|
Params: [{{{MemberDeclaration.GenerateDefs(Args)}}}],
|
|
ReturnType: {{{returnTypeExpr}}},
|
|
Visibility: SpacetimeDB.Internal.FunctionVisibility.ClientCallable
|
|
);
|
|
|
|
public byte[] Invoke(BinaryReader reader, SpacetimeDB.Internal.IProcedureContext ctx) {
|
|
{{{paramReads}}}{{{invokeBody}}}
|
|
}
|
|
}
|
|
""";
|
|
}
|
|
|
|
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.Identifier}")
|
|
)}}) {
|
|
using var stream = new MemoryStream();
|
|
using var writer = new BinaryWriter(stream);
|
|
{{string.Join(
|
|
"\n",
|
|
Args.Select(a => $"new {a.Type.ToBSATNString()}().Write(writer, {a.Identifier});")
|
|
)}}
|
|
SpacetimeDB.Internal.ProcedureExtensions.VolatileNonatomicScheduleImmediate(nameof({{Identifier}}), 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
|
|
{
|
|
private static string EscapeStringLiteral(string s) =>
|
|
s.Replace("\\", "\\\\")
|
|
.Replace("\"", "\\\"")
|
|
.Replace("\r", "\\r")
|
|
.Replace("\n", "\\n")
|
|
.Replace("\t", "\\t");
|
|
|
|
/// <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 settings = context
|
|
.SyntaxProvider.ForAttributeWithMetadataName(
|
|
fullyQualifiedMetadataName: typeof(SettingsAttribute).FullName,
|
|
predicate: (node, ct) => true,
|
|
transform: (context, ct) =>
|
|
context.ParseWithDiags(diag => new SettingsDeclaration(context, diag))
|
|
)
|
|
.ReportDiagnostics(context)
|
|
.WithTrackingName("SpacetimeDB.Settings.Parse");
|
|
|
|
var settingsArray = CollectDistinct(
|
|
"Settings",
|
|
context,
|
|
settings,
|
|
s => s.FullName,
|
|
s => s.FullName
|
|
);
|
|
|
|
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 tableDecls = CollectDistinct(
|
|
"TableDecl",
|
|
context,
|
|
tables,
|
|
t => t.FullName,
|
|
t => t.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, r.CanonicalName, Class: r.GenerateClass()))
|
|
.WithTrackingName("SpacetimeDB.Reducer.GenerateClass"),
|
|
r => r.Name,
|
|
r => r.FullName
|
|
);
|
|
|
|
var procedures = context
|
|
.SyntaxProvider.ForAttributeWithMetadataName(
|
|
fullyQualifiedMetadataName: typeof(ProcedureAttribute).FullName,
|
|
predicate: (node, ct) => true, // already covered by attribute restrictions
|
|
transform: (context, ct) =>
|
|
context.ParseWithDiags(diag => new ProcedureDeclaration(context, diag))
|
|
)
|
|
.ReportDiagnostics(context)
|
|
.WithTrackingName("SpacetimeDB.Procedure.Parse");
|
|
|
|
procedures
|
|
.Select((p, ct) => p.GenerateSchedule())
|
|
.WithTrackingName("SpacetimeDB.Procedure.GenerateSchedule")
|
|
.RegisterSourceOutputs(context);
|
|
|
|
var addProcedures = CollectDistinct(
|
|
"Procedure",
|
|
context,
|
|
procedures
|
|
.Select((p, ct) => (p.Name, p.FullName, p.CanonicalName, Class: p.GenerateClass()))
|
|
.WithTrackingName("SpacetimeDB.Procedure.GenerateClass"),
|
|
p => p.Name,
|
|
p => p.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(settingsArray)
|
|
.Combine(tableDecls)
|
|
.Combine(addReducers)
|
|
.Combine(addProcedures)
|
|
.Combine(readOnlyAccessors)
|
|
.Combine(views)
|
|
.Combine(rlsFiltersArray)
|
|
.Combine(columnDefaultValues),
|
|
(context, tuple) =>
|
|
{
|
|
var (
|
|
(
|
|
(
|
|
(
|
|
(
|
|
(((tableAccessors, settings), tableDecls), addReducers),
|
|
addProcedures
|
|
),
|
|
readOnlyAccessors
|
|
),
|
|
views
|
|
),
|
|
rlsFilters
|
|
),
|
|
columnDefaultValues
|
|
) = tuple;
|
|
|
|
if (settings.Array.Length > 1)
|
|
{
|
|
context.ReportDiagnostic(
|
|
ErrorDescriptor.DuplicateSettings.ToDiag(
|
|
settings.Array.Select(s => s.FullName)
|
|
)
|
|
);
|
|
}
|
|
|
|
var settingsRegistration =
|
|
settings.Array.Length == 1
|
|
&& settings.Array[0].CaseConversionPolicy is { } policyName
|
|
? $"SpacetimeDB.Internal.Module.SetCaseConversionPolicy(SpacetimeDB.CaseConversionPolicy.{policyName});"
|
|
: string.Empty;
|
|
|
|
var explicitTableRegistrations = string.Join(
|
|
"\n",
|
|
tableDecls.Array.SelectMany(t =>
|
|
t.TableAccessors.Where(a => !string.IsNullOrEmpty(a.CanonicalName))
|
|
.Select(a =>
|
|
$"SpacetimeDB.Internal.Module.RegisterExplicitTableName(\"{EscapeStringLiteral(a.Name)}\", \"{EscapeStringLiteral(a.CanonicalName!)}\");"
|
|
)
|
|
)
|
|
);
|
|
|
|
var explicitFunctionRegistrations = string.Join(
|
|
"\n",
|
|
addReducers
|
|
.Array.Where(r => !string.IsNullOrEmpty(r.CanonicalName))
|
|
.Select(r =>
|
|
$"SpacetimeDB.Internal.Module.RegisterExplicitFunctionName(\"{EscapeStringLiteral(r.Name)}\", \"{EscapeStringLiteral(r.CanonicalName!)}\");"
|
|
)
|
|
.Concat(
|
|
addProcedures
|
|
.Array.Where(p => !string.IsNullOrEmpty(p.CanonicalName))
|
|
.Select(p =>
|
|
$"SpacetimeDB.Internal.Module.RegisterExplicitFunctionName(\"{EscapeStringLiteral(p.Name)}\", \"{EscapeStringLiteral(p.CanonicalName!)}\");"
|
|
)
|
|
)
|
|
.Concat(
|
|
views
|
|
.Array.Where(v => !string.IsNullOrEmpty(v.CanonicalName))
|
|
.Select(v =>
|
|
$"SpacetimeDB.Internal.Module.RegisterExplicitFunctionName(\"{EscapeStringLiteral(v.Name)}\", \"{EscapeStringLiteral(v.CanonicalName!)}\");"
|
|
)
|
|
)
|
|
);
|
|
|
|
var explicitIndexRegistrations = string.Join(
|
|
"\n",
|
|
tableDecls.Array.SelectMany(t =>
|
|
t.TableAccessors.SelectMany(a =>
|
|
t.GetIndexes(a)
|
|
.Where(ix => !string.IsNullOrEmpty(ix.CanonicalName))
|
|
.Select(ix =>
|
|
$"SpacetimeDB.Internal.Module.RegisterExplicitIndexName(\"{EscapeStringLiteral(ix.StandardIndexName(a))}\", \"{EscapeStringLiteral(ix.CanonicalName!)}\");"
|
|
)
|
|
)
|
|
)
|
|
);
|
|
|
|
var preRegistrationLines = new[]
|
|
{
|
|
settingsRegistration,
|
|
explicitTableRegistrations,
|
|
explicitFunctionRegistrations,
|
|
explicitIndexRegistrations,
|
|
}
|
|
.Where(s => !string.IsNullOrWhiteSpace(s))
|
|
.ToArray();
|
|
|
|
var preRegistrations =
|
|
preRegistrationLines.Length == 0
|
|
? string.Empty
|
|
: "\n "
|
|
+ string.Join("\n ", preRegistrationLines)
|
|
+ "\n";
|
|
|
|
var queryBuilderMembers = string.Join(
|
|
"\n",
|
|
tableDecls.Array.SelectMany(t => t.GenerateQueryBuilderMembers())
|
|
);
|
|
// Don't generate the FFI boilerplate if there are no tables or reducers.
|
|
if (
|
|
tableAccessors.Array.IsEmpty
|
|
&& addReducers.Array.IsEmpty
|
|
&& addProcedures.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
|
|
#pragma warning disable STDB_UNSTABLE
|
|
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using Internal = SpacetimeDB.Internal;
|
|
using TxContext = SpacetimeDB.Internal.TxContext;
|
|
|
|
namespace SpacetimeDB {
|
|
{{queryBuilderMembers}}
|
|
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;
|
|
// **Note:** must be 0..=u32::MAX
|
|
internal int CounterUuid;
|
|
// 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, AuthCtx? senderAuth = null)
|
|
{
|
|
Sender = identity;
|
|
ConnectionId = connectionId;
|
|
Rng = random;
|
|
Timestamp = time;
|
|
SenderAuth = senderAuth ?? AuthCtx.BuildFromSystemTables(connectionId, identity);
|
|
CounterUuid = 0;
|
|
}
|
|
/// <summary>
|
|
/// Create a new random <see cref="Uuid"/> `v4` using the built-in RNG.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method fills the random bytes using the context RNG.
|
|
/// </remarks>
|
|
/// <example>
|
|
/// <code>
|
|
/// var uuid = ctx.NewUuidV4();
|
|
/// Log.Info(uuid);
|
|
/// </code>
|
|
/// </example>
|
|
public Uuid NewUuidV4()
|
|
{
|
|
var bytes = new byte[16];
|
|
Rng.NextBytes(bytes);
|
|
return Uuid.FromRandomBytesV4(bytes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a new sortable <see cref="Uuid"/> `v7` using the built-in RNG, monotonic counter,
|
|
/// and timestamp.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A newly generated <see cref="Uuid"/> `v7` that is monotonically ordered
|
|
/// and suitable for use as a primary key or for ordered storage.
|
|
/// </returns>
|
|
/// <exception cref="Exception">
|
|
/// Thrown if <see cref="Uuid"/> generation fails.
|
|
/// </exception>
|
|
/// <example>
|
|
/// <code>
|
|
/// [SpacetimeDB.Reducer]
|
|
/// public static Guid GenerateUuidV7(ReducerContext ctx)
|
|
/// {
|
|
/// Guid uuid = ctx.NewUuidV7();
|
|
/// Log.Info(uuid);
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public Uuid NewUuidV7()
|
|
{
|
|
var bytes = new byte[4];
|
|
Rng.NextBytes(bytes);
|
|
return Uuid.FromCounterV7(ref CounterUuid, Timestamp, bytes);
|
|
}
|
|
}
|
|
|
|
public sealed partial class ProcedureContext : global::SpacetimeDB.ProcedureContextBase {
|
|
private readonly Local _db = new();
|
|
|
|
internal ProcedureContext(Identity identity, ConnectionId? connectionId, Random random, Timestamp time)
|
|
: base(identity, connectionId, random, time) {}
|
|
|
|
protected override global::SpacetimeDB.LocalBase CreateLocal() => _db;
|
|
protected override global::SpacetimeDB.ProcedureTxContextBase CreateTxContext(Internal.TxContext inner) =>
|
|
_cached ??= new ProcedureTxContext(inner);
|
|
|
|
private ProcedureTxContext? _cached;
|
|
|
|
[Experimental("STDB_UNSTABLE")]
|
|
public Local Db => _db;
|
|
|
|
[Experimental("STDB_UNSTABLE")]
|
|
public TResult WithTx<TResult>(Func<ProcedureTxContext, TResult> body) =>
|
|
base.WithTx(tx => body((ProcedureTxContext)tx));
|
|
|
|
[Experimental("STDB_UNSTABLE")]
|
|
public TxOutcome<TResult> TryWithTx<TResult, TError>(
|
|
Func<ProcedureTxContext, Result<TResult, TError>> body)
|
|
where TError : Exception =>
|
|
base.TryWithTx(tx => body((ProcedureTxContext)tx));
|
|
|
|
/// <summary>
|
|
/// Create a new random <see cref="Uuid"/> `v4` using the built-in RNG.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method fills the random bytes using the context RNG.
|
|
/// </remarks>
|
|
/// <example>
|
|
/// <code>
|
|
/// var uuid = ctx.NewUuidV4();
|
|
/// Log.Info(uuid);
|
|
/// </code>
|
|
/// </example>
|
|
public Uuid NewUuidV4()
|
|
{
|
|
var bytes = new byte[16];
|
|
Rng.NextBytes(bytes);
|
|
return Uuid.FromRandomBytesV4(bytes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a new sortable <see cref="Uuid"/> `v7` using the built-in RNG, monotonic counter,
|
|
/// and timestamp.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A newly generated <see cref="Uuid"/> `v7` that is monotonically ordered
|
|
/// and suitable for use as a primary key or for ordered storage.
|
|
/// </returns>
|
|
/// <exception cref="Exception">
|
|
/// Thrown if UUID generation fails.
|
|
/// </exception>
|
|
/// <example>
|
|
/// <code>
|
|
/// [SpacetimeDB.Procedure]
|
|
/// public static Guid GenerateUuidV7(ReducerContext ctx)
|
|
/// {
|
|
/// Guid uuid = ctx.NewUuidV7();
|
|
/// Log.Info(uuid);
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public Uuid NewUuidV7()
|
|
{
|
|
var bytes = new byte[4];
|
|
Rng.NextBytes(bytes);
|
|
return Uuid.FromCounterV7(ref CounterUuid, Timestamp, bytes);
|
|
}
|
|
}
|
|
|
|
[Experimental("STDB_UNSTABLE")]
|
|
public sealed class ProcedureTxContext : global::SpacetimeDB.ProcedureTxContextBase {
|
|
internal ProcedureTxContext(Internal.TxContext inner) : base(inner) {}
|
|
|
|
public new Local Db => (Local)base.Db;
|
|
}
|
|
|
|
public sealed class Local : global::SpacetimeDB.LocalBase {
|
|
{{string.Join("\n", tableAccessors.Select(v => v.getter))}}
|
|
}
|
|
|
|
public sealed record ViewContext : DbContext<Internal.LocalReadOnly>, Internal.IViewContext
|
|
{
|
|
public Identity Sender { get; }
|
|
|
|
public QueryBuilder From => default;
|
|
|
|
internal ViewContext(Identity sender, Internal.LocalReadOnly db)
|
|
: base(db)
|
|
{
|
|
Sender = sender;
|
|
}
|
|
}
|
|
|
|
public sealed record AnonymousViewContext : DbContext<Internal.LocalReadOnly>, Internal.IAnonymousViewContext
|
|
{
|
|
public QueryBuilder From => default;
|
|
|
|
internal AnonymousViewContext(Internal.LocalReadOnly db)
|
|
: base(db) { }
|
|
}
|
|
}
|
|
|
|
namespace SpacetimeDB.Internal.TableHandles {
|
|
{{string.Join("\n", tableAccessors.Select(v => v.tableAccessor))}}
|
|
}
|
|
|
|
{{string.Join("\n",
|
|
views.Array.Where(v => !v.IsAnonymous)
|
|
.Select((v, i) => v.GenerateDispatcherClass((uint)i))
|
|
.Concat(
|
|
views.Array.Where(v => v.IsAnonymous)
|
|
.Select((v, i) => v.GenerateDispatcherClass((uint)i))
|
|
)
|
|
)}}
|
|
|
|
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))}}
|
|
|
|
{{string.Join("\n", addProcedures.Select(r => r.Class))}}
|
|
|
|
public static List<T> ToListOrEmpty<T>(T? value) where T : struct
|
|
=> value is null ? new List<T>() : new List<T> { value.Value };
|
|
|
|
public static List<T> ToListOrEmpty<T>(T? value) where T : class
|
|
=> value is null ? new List<T>() : new List<T> { value };
|
|
|
|
#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()));
|
|
SpacetimeDB.Internal.Module.SetProcedureContextConstructor((identity, connectionId, random, time) => new SpacetimeDB.ProcedureContext(identity, connectionId, random, time));{{preRegistrations}}
|
|
var __memoryStream = new MemoryStream();
|
|
var __writer = new BinaryWriter(__memoryStream);
|
|
|
|
{{string.Join(
|
|
"\n",
|
|
addReducers.Select(r =>
|
|
$"SpacetimeDB.Internal.Module.RegisterReducer<{EscapeIdentifier(r.Name)}>();"
|
|
)
|
|
)}}
|
|
{{string.Join(
|
|
"\n",
|
|
addProcedures.Select(r =>
|
|
$"SpacetimeDB.Internal.Module.RegisterProcedure<{EscapeIdentifier(r.Name)}>();"
|
|
)
|
|
)}}
|
|
|
|
// IMPORTANT: The order in which we register views matters.
|
|
// It must correspond to the order in which we call `GenerateDispatcherClass`.
|
|
// See the comment on `GenerateDispatcherClass` for more explanation.
|
|
{{string.Join("\n",
|
|
views.Array.Where(v => !v.IsAnonymous)
|
|
.Select(v => $"SpacetimeDB.Internal.Module.RegisterView<{v.Name}ViewDispatcher>();")
|
|
.Concat(
|
|
views.Array.Where(v => v.IsAnonymous)
|
|
.Select(v => $"SpacetimeDB.Internal.Module.RegisterAnonymousView<{v.Name}ViewDispatcher>();")
|
|
)
|
|
)}}
|
|
|
|
{{string.Join(
|
|
"\n",
|
|
tableAccessors.Select(t => $"SpacetimeDB.Internal.Module.RegisterTable<{t.tableName}, SpacetimeDB.Internal.TableHandles.{EscapeIdentifier(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
|
|
);
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "__call_procedure__")]
|
|
public static SpacetimeDB.Internal.Errno __call_procedure__(
|
|
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 result_sink
|
|
) => SpacetimeDB.Internal.Module.__call_procedure__(
|
|
id,
|
|
sender_0,
|
|
sender_1,
|
|
sender_2,
|
|
sender_3,
|
|
conn_id_0,
|
|
conn_id_1,
|
|
timestamp,
|
|
args,
|
|
result_sink
|
|
);
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "__call_view__")]
|
|
public static SpacetimeDB.Internal.Errno __call_view__(
|
|
uint id,
|
|
ulong sender_0,
|
|
ulong sender_1,
|
|
ulong sender_2,
|
|
ulong sender_3,
|
|
SpacetimeDB.Internal.BytesSource args,
|
|
SpacetimeDB.Internal.BytesSink sink
|
|
) => SpacetimeDB.Internal.Module.__call_view__(
|
|
id,
|
|
sender_0,
|
|
sender_1,
|
|
sender_2,
|
|
sender_3,
|
|
args,
|
|
sink
|
|
);
|
|
|
|
[UnmanagedCallersOnly(EntryPoint = "__call_view_anon__")]
|
|
public static SpacetimeDB.Internal.Errno __call_view_anon__(
|
|
uint id,
|
|
SpacetimeDB.Internal.BytesSource args,
|
|
SpacetimeDB.Internal.BytesSink sink
|
|
) => SpacetimeDB.Internal.Module.__call_view_anon__(
|
|
id,
|
|
args,
|
|
sink
|
|
);
|
|
#endif
|
|
}
|
|
|
|
#pragma warning restore STDB_UNSTABLE
|
|
#pragma warning restore CS0436
|
|
"""
|
|
);
|
|
}
|
|
);
|
|
}
|
|
}
|