From b3ec33a0e36642e6d6bc80e1f5e265d58492442c Mon Sep 17 00:00:00 2001
From: Ingvar Stepanyan <me@rreverser.com>
Date: Mon, 16 Dec 2024 18:11:49 +0000
Subject: [PATCH] More fixes

---
 crates/bindings-csharp/BSATN.Codegen/Diag.cs  |  16 +
 .../Codegen.Tests/fixtures/diag/Lib.cs        |  46 +-
 .../ExtraCompilationErrors.verified.txt       |   4 +-
 .../diag/snapshots/Module#FFI.verified.cs     | 423 ++++++++++++------
 ...estIncompatibleScheduleReducer.verified.cs |  20 -
 ...odule#TestIncompatibleSchedule.verified.cs |  53 ---
 .../diag/snapshots/Module.verified.txt        | 117 +++--
 .../Codegen.Tests/fixtures/server/Lib.cs      |   4 +
 .../server/snapshots/Module#FFI.verified.cs   |  10 +-
 ...Module#Timers.SendMessageTimer.verified.cs |  14 +-
 crates/bindings-csharp/Codegen/Diag.cs        |   6 +
 crates/bindings-csharp/Codegen/Module.cs      |  32 +-
 crates/bindings-csharp/Runtime/Attrs.cs       |   2 +-
 13 files changed, 470 insertions(+), 277 deletions(-)
 delete mode 100644 crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#TestIncompatibleSchedule.TestIncompatibleScheduleReducer.verified.cs
 delete mode 100644 crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#TestIncompatibleSchedule.verified.cs

diff --git a/crates/bindings-csharp/BSATN.Codegen/Diag.cs b/crates/bindings-csharp/BSATN.Codegen/Diag.cs
index 7da994bd64..2ae3b2513b 100644
--- a/crates/bindings-csharp/BSATN.Codegen/Diag.cs
+++ b/crates/bindings-csharp/BSATN.Codegen/Diag.cs
@@ -91,6 +91,22 @@ Func<TContext, ISymbol> toLocation
     )
         : this(group, title, interpolate, ctx => toLocation(ctx).Locations.FirstOrDefault()) { }
 
+    public ErrorDescriptor(
+        ErrorDescriptorGroup group,
+        string title,
+        Expression<Func<TContext, FormattableString>> interpolate,
+        Func<TContext, AttributeData> toLocation
+    )
+        : this(
+            group,
+            title,
+            interpolate,
+            ctx =>
+                toLocation(ctx).ApplicationSyntaxReference is { } r
+                    ? r.SyntaxTree.GetLocation(r.Span)
+                    : null
+        ) { }
+
     public Diagnostic ToDiag(TContext ctx) =>
         Diagnostic.Create(descriptor, toLocation(ctx), makeFormatArgs(ctx));
 }
diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/Lib.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/Lib.cs
index 61423e76db..8b485188a7 100644
--- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/Lib.cs
+++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/Lib.cs
@@ -361,20 +361,6 @@ public static partial class InAnotherNamespace
     public partial struct TestDuplicateTableName { }
 }
 
-[SpacetimeDB.Table(
-    Name = "TestIncompatibleSchedule1",
-    Scheduled = nameof(TestIncompatibleScheduleReducer)
-)]
-[SpacetimeDB.Table(Name = "TestIncompatibleSchedule2")]
-public partial struct TestIncompatibleSchedule
-{
-    [SpacetimeDB.Reducer]
-    public static void TestIncompatibleScheduleReducer(
-        ReducerContext ctx,
-        TestIncompatibleSchedule table
-    ) { }
-}
-
 [SpacetimeDB.Table]
 [SpacetimeDB.Index]
 public partial struct TestIndexWithoutColumns { }
@@ -382,3 +368,35 @@ public partial struct TestIndexWithoutColumns { }
 [SpacetimeDB.Table]
 [SpacetimeDB.Index(BTree = [])]
 public partial struct TestIndexWithEmptyColumns { }
+
+[SpacetimeDB.Table(
+    Name = "TestScheduleWithoutPrimaryKey",
+    Scheduled = "DummyScheduledReducer",
+    ScheduledAt = nameof(ScheduleAtCorrectType)
+)]
+[SpacetimeDB.Table(
+    Name = "TestScheduleWithWrongPrimaryKeyType",
+    Scheduled = "DummyScheduledReducer",
+    ScheduledAt = nameof(ScheduleAtCorrectType)
+)]
+[SpacetimeDB.Table(Name = "TestScheduleWithoutScheduleAt", Scheduled = "DummyScheduledReducer")]
+[SpacetimeDB.Table(
+    Name = "TestScheduleWithWrongScheduleAtType",
+    Scheduled = "DummyScheduledReducer",
+    ScheduledAt = nameof(ScheduleAtWrongType)
+)]
+public partial struct TestScheduleIssues
+{
+    [SpacetimeDB.PrimaryKey(Table = "TestScheduleWithWrongPrimaryKeyType")]
+    public string IdWrongType;
+
+    [SpacetimeDB.PrimaryKey(Table = "TestScheduleWithoutScheduleAt")]
+    [SpacetimeDB.PrimaryKey(Table = "TestScheduleWithWrongScheduleAtType")]
+    public int IdCorrectType;
+
+    public int ScheduleAtWrongType;
+    public ScheduleAt ScheduleAtCorrectType;
+
+    [SpacetimeDB.Reducer]
+    public static void DummyScheduledReducer(ReducerContext ctx, TestScheduleIssues table) { }
+}
diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/ExtraCompilationErrors.verified.txt b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/ExtraCompilationErrors.verified.txt
index a61c60ca17..465227b1cc 100644
--- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/ExtraCompilationErrors.verified.txt
+++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/ExtraCompilationErrors.verified.txt
@@ -805,7 +805,7 @@ public TestUniqueNotEquatableUniqueIndex PrimaryKeyField => new(this);
     }
   },
   {/*
-SpacetimeDB.Internal.Module.RegisterTable<global::TestIncompatibleSchedule, SpacetimeDB.Internal.TableHandles.TestIncompatibleSchedule2>();
+SpacetimeDB.Internal.Module.RegisterTable<global::TestScheduleIssues, SpacetimeDB.Internal.TableHandles.TestScheduleWithWrongScheduleAtType>();
 SpacetimeDB.Internal.Module.RegisterTable<global::TestTableTaggedEnum, SpacetimeDB.Internal.TableHandles.TestTableTaggedEnum>();
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 SpacetimeDB.Internal.Module.RegisterTable<global::TestUniqueNotEquatable, SpacetimeDB.Internal.TableHandles.TestUniqueNotEquatable>();
@@ -828,7 +828,7 @@ SpacetimeDB.Internal.Module.RegisterTable<global::TestUniqueNotEquatable, Spacet
     }
   },
   {/*
-SpacetimeDB.Internal.Module.RegisterTable<global::TestIncompatibleSchedule, SpacetimeDB.Internal.TableHandles.TestIncompatibleSchedule2>();
+SpacetimeDB.Internal.Module.RegisterTable<global::TestScheduleIssues, SpacetimeDB.Internal.TableHandles.TestScheduleWithWrongScheduleAtType>();
 SpacetimeDB.Internal.Module.RegisterTable<global::TestTableTaggedEnum, SpacetimeDB.Internal.TableHandles.TestTableTaggedEnum>();
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 SpacetimeDB.Internal.Module.RegisterTable<global::TestUniqueNotEquatable, SpacetimeDB.Internal.TableHandles.TestUniqueNotEquatable>();
diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs
index 46b372df9c..a3c89ede44 100644
--- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs
+++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs
@@ -198,219 +198,352 @@ public bool Delete(global::TestDuplicateTableName row) =>
                 >.DoDelete(row);
         }
 
-        public readonly struct TestIncompatibleSchedule1
+        public readonly struct TestScheduleWithoutPrimaryKey
             : SpacetimeDB.Internal.ITableView<
-                TestIncompatibleSchedule1,
-                global::TestIncompatibleSchedule
+                TestScheduleWithoutPrimaryKey,
+                global::TestScheduleIssues
             >
         {
-            static global::TestIncompatibleSchedule SpacetimeDB.Internal.ITableView<
-                TestIncompatibleSchedule1,
-                global::TestIncompatibleSchedule
-            >.ReadGenFields(System.IO.BinaryReader reader, global::TestIncompatibleSchedule row)
+            static global::TestScheduleIssues SpacetimeDB.Internal.ITableView<
+                TestScheduleWithoutPrimaryKey,
+                global::TestScheduleIssues
+            >.ReadGenFields(System.IO.BinaryReader reader, global::TestScheduleIssues row)
             {
-                if (row.ScheduledId == default)
-                {
-                    row.ScheduledId = global::TestIncompatibleSchedule.BSATN.ScheduledId.Read(
-                        reader
-                    );
-                }
                 return row;
             }
 
             static SpacetimeDB.Internal.RawTableDefV9 SpacetimeDB.Internal.ITableView<
-                TestIncompatibleSchedule1,
-                global::TestIncompatibleSchedule
+                TestScheduleWithoutPrimaryKey,
+                global::TestScheduleIssues
             >.MakeTableDesc(SpacetimeDB.BSATN.ITypeRegistrar registrar) =>
                 new(
-                    Name: nameof(TestIncompatibleSchedule1),
+                    Name: nameof(TestScheduleWithoutPrimaryKey),
                     ProductTypeRef: (uint)
-                        new global::TestIncompatibleSchedule.BSATN()
-                            .GetAlgebraicType(registrar)
-                            .Ref_,
-                    PrimaryKey: [0],
+                        new global::TestScheduleIssues.BSATN().GetAlgebraicType(registrar).Ref_,
+                    PrimaryKey: [],
+                    Indexes: [],
+                    Constraints: [],
+                    Sequences: [],
+                    Schedule: SpacetimeDB.Internal.ITableView<
+                        TestScheduleWithoutPrimaryKey,
+                        global::TestScheduleIssues
+                    >.MakeSchedule("DummyScheduledReducer", 3),
+                    TableType: SpacetimeDB.Internal.TableType.User,
+                    TableAccess: SpacetimeDB.Internal.TableAccess.Private
+                );
+
+            public ulong Count =>
+                SpacetimeDB.Internal.ITableView<
+                    TestScheduleWithoutPrimaryKey,
+                    global::TestScheduleIssues
+                >.DoCount();
+
+            public IEnumerable<global::TestScheduleIssues> Iter() =>
+                SpacetimeDB.Internal.ITableView<
+                    TestScheduleWithoutPrimaryKey,
+                    global::TestScheduleIssues
+                >.DoIter();
+
+            public global::TestScheduleIssues Insert(global::TestScheduleIssues row) =>
+                SpacetimeDB.Internal.ITableView<
+                    TestScheduleWithoutPrimaryKey,
+                    global::TestScheduleIssues
+                >.DoInsert(row);
+
+            public bool Delete(global::TestScheduleIssues row) =>
+                SpacetimeDB.Internal.ITableView<
+                    TestScheduleWithoutPrimaryKey,
+                    global::TestScheduleIssues
+                >.DoDelete(row);
+        }
+
+        public readonly struct TestScheduleWithoutScheduleAt
+            : SpacetimeDB.Internal.ITableView<
+                TestScheduleWithoutScheduleAt,
+                global::TestScheduleIssues
+            >
+        {
+            static global::TestScheduleIssues SpacetimeDB.Internal.ITableView<
+                TestScheduleWithoutScheduleAt,
+                global::TestScheduleIssues
+            >.ReadGenFields(System.IO.BinaryReader reader, global::TestScheduleIssues row)
+            {
+                return row;
+            }
+
+            static SpacetimeDB.Internal.RawTableDefV9 SpacetimeDB.Internal.ITableView<
+                TestScheduleWithoutScheduleAt,
+                global::TestScheduleIssues
+            >.MakeTableDesc(SpacetimeDB.BSATN.ITypeRegistrar registrar) =>
+                new(
+                    Name: nameof(TestScheduleWithoutScheduleAt),
+                    ProductTypeRef: (uint)
+                        new global::TestScheduleIssues.BSATN().GetAlgebraicType(registrar).Ref_,
+                    PrimaryKey: [1],
                     Indexes:
                     [
                         new(
                             Name: null,
-                            AccessorName: "ScheduledId",
-                            Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0])
+                            AccessorName: "IdCorrectType",
+                            Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([1])
                         )
                     ],
                     Constraints:
                     [
                         SpacetimeDB.Internal.ITableView<
-                            TestIncompatibleSchedule1,
-                            global::TestIncompatibleSchedule
-                        >.MakeUniqueConstraint(0)
-                    ],
-                    Sequences:
-                    [
-                        SpacetimeDB.Internal.ITableView<
-                            TestIncompatibleSchedule1,
-                            global::TestIncompatibleSchedule
-                        >.MakeSequence(0)
+                            TestScheduleWithoutScheduleAt,
+                            global::TestScheduleIssues
+                        >.MakeUniqueConstraint(1)
                     ],
-                    Schedule: SpacetimeDB.Internal.ITableView<
-                        TestIncompatibleSchedule1,
-                        global::TestIncompatibleSchedule
-                    >.MakeSchedule("TestIncompatibleScheduleReducer", 1),
+                    Sequences: [],
+                    Schedule: null,
                     TableType: SpacetimeDB.Internal.TableType.User,
                     TableAccess: SpacetimeDB.Internal.TableAccess.Private
                 );
 
             public ulong Count =>
                 SpacetimeDB.Internal.ITableView<
-                    TestIncompatibleSchedule1,
-                    global::TestIncompatibleSchedule
+                    TestScheduleWithoutScheduleAt,
+                    global::TestScheduleIssues
                 >.DoCount();
 
-            public IEnumerable<global::TestIncompatibleSchedule> Iter() =>
+            public IEnumerable<global::TestScheduleIssues> Iter() =>
                 SpacetimeDB.Internal.ITableView<
-                    TestIncompatibleSchedule1,
-                    global::TestIncompatibleSchedule
+                    TestScheduleWithoutScheduleAt,
+                    global::TestScheduleIssues
                 >.DoIter();
 
-            public global::TestIncompatibleSchedule Insert(global::TestIncompatibleSchedule row) =>
+            public global::TestScheduleIssues Insert(global::TestScheduleIssues row) =>
                 SpacetimeDB.Internal.ITableView<
-                    TestIncompatibleSchedule1,
-                    global::TestIncompatibleSchedule
+                    TestScheduleWithoutScheduleAt,
+                    global::TestScheduleIssues
                 >.DoInsert(row);
 
-            public bool Delete(global::TestIncompatibleSchedule row) =>
+            public bool Delete(global::TestScheduleIssues row) =>
                 SpacetimeDB.Internal.ITableView<
-                    TestIncompatibleSchedule1,
-                    global::TestIncompatibleSchedule
+                    TestScheduleWithoutScheduleAt,
+                    global::TestScheduleIssues
                 >.DoDelete(row);
 
-            public sealed class TestIncompatibleSchedule1UniqueIndex
+            public sealed class TestScheduleWithoutScheduleAtUniqueIndex
                 : UniqueIndex<
-                    TestIncompatibleSchedule1,
-                    global::TestIncompatibleSchedule,
-                    ulong,
-                    SpacetimeDB.BSATN.U64
+                    TestScheduleWithoutScheduleAt,
+                    global::TestScheduleIssues,
+                    int,
+                    SpacetimeDB.BSATN.I32
                 >
             {
-                internal TestIncompatibleSchedule1UniqueIndex(TestIncompatibleSchedule1 handle)
-                    : base(handle, "TestIncompatibleSchedule1_ScheduledId_idx_btree") { }
+                internal TestScheduleWithoutScheduleAtUniqueIndex(
+                    TestScheduleWithoutScheduleAt handle
+                )
+                    : base(handle, "TestScheduleWithoutScheduleAt_IdCorrectType_idx_btree") { }
 
                 // 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 global::TestIncompatibleSchedule? Find(ulong key) =>
-                    DoFilter(key).Cast<global::TestIncompatibleSchedule?>().SingleOrDefault();
+                public global::TestScheduleIssues? Find(int key) =>
+                    DoFilter(key).Cast<global::TestScheduleIssues?>().SingleOrDefault();
 
-                public bool Update(global::TestIncompatibleSchedule row) =>
-                    DoUpdate(row.ScheduledId, row);
+                public bool Update(global::TestScheduleIssues row) =>
+                    DoUpdate(row.IdCorrectType, row);
             }
 
-            public TestIncompatibleSchedule1UniqueIndex ScheduledId => new(this);
+            public TestScheduleWithoutScheduleAtUniqueIndex IdCorrectType => new(this);
         }
 
-        public readonly struct TestIncompatibleSchedule2
+        public readonly struct TestScheduleWithWrongPrimaryKeyType
             : SpacetimeDB.Internal.ITableView<
-                TestIncompatibleSchedule2,
-                global::TestIncompatibleSchedule
+                TestScheduleWithWrongPrimaryKeyType,
+                global::TestScheduleIssues
             >
         {
-            static global::TestIncompatibleSchedule SpacetimeDB.Internal.ITableView<
-                TestIncompatibleSchedule2,
-                global::TestIncompatibleSchedule
-            >.ReadGenFields(System.IO.BinaryReader reader, global::TestIncompatibleSchedule row)
+            static global::TestScheduleIssues SpacetimeDB.Internal.ITableView<
+                TestScheduleWithWrongPrimaryKeyType,
+                global::TestScheduleIssues
+            >.ReadGenFields(System.IO.BinaryReader reader, global::TestScheduleIssues row)
             {
-                if (row.ScheduledId == default)
-                {
-                    row.ScheduledId = global::TestIncompatibleSchedule.BSATN.ScheduledId.Read(
-                        reader
-                    );
-                }
                 return row;
             }
 
             static SpacetimeDB.Internal.RawTableDefV9 SpacetimeDB.Internal.ITableView<
-                TestIncompatibleSchedule2,
-                global::TestIncompatibleSchedule
+                TestScheduleWithWrongPrimaryKeyType,
+                global::TestScheduleIssues
             >.MakeTableDesc(SpacetimeDB.BSATN.ITypeRegistrar registrar) =>
                 new(
-                    Name: nameof(TestIncompatibleSchedule2),
+                    Name: nameof(TestScheduleWithWrongPrimaryKeyType),
                     ProductTypeRef: (uint)
-                        new global::TestIncompatibleSchedule.BSATN()
-                            .GetAlgebraicType(registrar)
-                            .Ref_,
+                        new global::TestScheduleIssues.BSATN().GetAlgebraicType(registrar).Ref_,
                     PrimaryKey: [0],
                     Indexes:
                     [
                         new(
                             Name: null,
-                            AccessorName: "ScheduledId",
+                            AccessorName: "IdWrongType",
                             Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0])
                         )
                     ],
                     Constraints:
                     [
                         SpacetimeDB.Internal.ITableView<
-                            TestIncompatibleSchedule2,
-                            global::TestIncompatibleSchedule
+                            TestScheduleWithWrongPrimaryKeyType,
+                            global::TestScheduleIssues
                         >.MakeUniqueConstraint(0)
                     ],
-                    Sequences:
+                    Sequences: [],
+                    Schedule: SpacetimeDB.Internal.ITableView<
+                        TestScheduleWithWrongPrimaryKeyType,
+                        global::TestScheduleIssues
+                    >.MakeSchedule("DummyScheduledReducer", 3),
+                    TableType: SpacetimeDB.Internal.TableType.User,
+                    TableAccess: SpacetimeDB.Internal.TableAccess.Private
+                );
+
+            public ulong Count =>
+                SpacetimeDB.Internal.ITableView<
+                    TestScheduleWithWrongPrimaryKeyType,
+                    global::TestScheduleIssues
+                >.DoCount();
+
+            public IEnumerable<global::TestScheduleIssues> Iter() =>
+                SpacetimeDB.Internal.ITableView<
+                    TestScheduleWithWrongPrimaryKeyType,
+                    global::TestScheduleIssues
+                >.DoIter();
+
+            public global::TestScheduleIssues Insert(global::TestScheduleIssues row) =>
+                SpacetimeDB.Internal.ITableView<
+                    TestScheduleWithWrongPrimaryKeyType,
+                    global::TestScheduleIssues
+                >.DoInsert(row);
+
+            public bool Delete(global::TestScheduleIssues row) =>
+                SpacetimeDB.Internal.ITableView<
+                    TestScheduleWithWrongPrimaryKeyType,
+                    global::TestScheduleIssues
+                >.DoDelete(row);
+
+            public sealed class TestScheduleWithWrongPrimaryKeyTypeUniqueIndex
+                : UniqueIndex<
+                    TestScheduleWithWrongPrimaryKeyType,
+                    global::TestScheduleIssues,
+                    string,
+                    SpacetimeDB.BSATN.String
+                >
+            {
+                internal TestScheduleWithWrongPrimaryKeyTypeUniqueIndex(
+                    TestScheduleWithWrongPrimaryKeyType handle
+                )
+                    : base(handle, "TestScheduleWithWrongPrimaryKeyType_IdWrongType_idx_btree") { }
+
+                // 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 global::TestScheduleIssues? Find(string key) =>
+                    DoFilter(key).Cast<global::TestScheduleIssues?>().SingleOrDefault();
+
+                public bool Update(global::TestScheduleIssues row) =>
+                    DoUpdate(row.IdWrongType, row);
+            }
+
+            public TestScheduleWithWrongPrimaryKeyTypeUniqueIndex IdWrongType => new(this);
+        }
+
+        public readonly struct TestScheduleWithWrongScheduleAtType
+            : SpacetimeDB.Internal.ITableView<
+                TestScheduleWithWrongScheduleAtType,
+                global::TestScheduleIssues
+            >
+        {
+            static global::TestScheduleIssues SpacetimeDB.Internal.ITableView<
+                TestScheduleWithWrongScheduleAtType,
+                global::TestScheduleIssues
+            >.ReadGenFields(System.IO.BinaryReader reader, global::TestScheduleIssues row)
+            {
+                return row;
+            }
+
+            static SpacetimeDB.Internal.RawTableDefV9 SpacetimeDB.Internal.ITableView<
+                TestScheduleWithWrongScheduleAtType,
+                global::TestScheduleIssues
+            >.MakeTableDesc(SpacetimeDB.BSATN.ITypeRegistrar registrar) =>
+                new(
+                    Name: nameof(TestScheduleWithWrongScheduleAtType),
+                    ProductTypeRef: (uint)
+                        new global::TestScheduleIssues.BSATN().GetAlgebraicType(registrar).Ref_,
+                    PrimaryKey: [1],
+                    Indexes:
+                    [
+                        new(
+                            Name: null,
+                            AccessorName: "IdCorrectType",
+                            Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([1])
+                        )
+                    ],
+                    Constraints:
                     [
                         SpacetimeDB.Internal.ITableView<
-                            TestIncompatibleSchedule2,
-                            global::TestIncompatibleSchedule
-                        >.MakeSequence(0)
+                            TestScheduleWithWrongScheduleAtType,
+                            global::TestScheduleIssues
+                        >.MakeUniqueConstraint(1)
                     ],
-                    Schedule: null,
+                    Sequences: [],
+                    Schedule: SpacetimeDB.Internal.ITableView<
+                        TestScheduleWithWrongScheduleAtType,
+                        global::TestScheduleIssues
+                    >.MakeSchedule("DummyScheduledReducer", 2),
                     TableType: SpacetimeDB.Internal.TableType.User,
                     TableAccess: SpacetimeDB.Internal.TableAccess.Private
                 );
 
             public ulong Count =>
                 SpacetimeDB.Internal.ITableView<
-                    TestIncompatibleSchedule2,
-                    global::TestIncompatibleSchedule
+                    TestScheduleWithWrongScheduleAtType,
+                    global::TestScheduleIssues
                 >.DoCount();
 
-            public IEnumerable<global::TestIncompatibleSchedule> Iter() =>
+            public IEnumerable<global::TestScheduleIssues> Iter() =>
                 SpacetimeDB.Internal.ITableView<
-                    TestIncompatibleSchedule2,
-                    global::TestIncompatibleSchedule
+                    TestScheduleWithWrongScheduleAtType,
+                    global::TestScheduleIssues
                 >.DoIter();
 
-            public global::TestIncompatibleSchedule Insert(global::TestIncompatibleSchedule row) =>
+            public global::TestScheduleIssues Insert(global::TestScheduleIssues row) =>
                 SpacetimeDB.Internal.ITableView<
-                    TestIncompatibleSchedule2,
-                    global::TestIncompatibleSchedule
+                    TestScheduleWithWrongScheduleAtType,
+                    global::TestScheduleIssues
                 >.DoInsert(row);
 
-            public bool Delete(global::TestIncompatibleSchedule row) =>
+            public bool Delete(global::TestScheduleIssues row) =>
                 SpacetimeDB.Internal.ITableView<
-                    TestIncompatibleSchedule2,
-                    global::TestIncompatibleSchedule
+                    TestScheduleWithWrongScheduleAtType,
+                    global::TestScheduleIssues
                 >.DoDelete(row);
 
-            public sealed class TestIncompatibleSchedule2UniqueIndex
+            public sealed class TestScheduleWithWrongScheduleAtTypeUniqueIndex
                 : UniqueIndex<
-                    TestIncompatibleSchedule2,
-                    global::TestIncompatibleSchedule,
-                    ulong,
-                    SpacetimeDB.BSATN.U64
+                    TestScheduleWithWrongScheduleAtType,
+                    global::TestScheduleIssues,
+                    int,
+                    SpacetimeDB.BSATN.I32
                 >
             {
-                internal TestIncompatibleSchedule2UniqueIndex(TestIncompatibleSchedule2 handle)
-                    : base(handle, "TestIncompatibleSchedule2_ScheduledId_idx_btree") { }
+                internal TestScheduleWithWrongScheduleAtTypeUniqueIndex(
+                    TestScheduleWithWrongScheduleAtType handle
+                )
+                    : base(handle, "TestScheduleWithWrongScheduleAtType_IdCorrectType_idx_btree")
+                { }
 
                 // 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 global::TestIncompatibleSchedule? Find(ulong key) =>
-                    DoFilter(key).Cast<global::TestIncompatibleSchedule?>().SingleOrDefault();
+                public global::TestScheduleIssues? Find(int key) =>
+                    DoFilter(key).Cast<global::TestScheduleIssues?>().SingleOrDefault();
 
-                public bool Update(global::TestIncompatibleSchedule row) =>
-                    DoUpdate(row.ScheduledId, row);
+                public bool Update(global::TestScheduleIssues row) =>
+                    DoUpdate(row.IdCorrectType, row);
             }
 
-            public TestIncompatibleSchedule2UniqueIndex ScheduledId => new(this);
+            public TestScheduleWithWrongScheduleAtTypeUniqueIndex IdCorrectType => new(this);
         }
 
         public readonly struct TestTableTaggedEnum
@@ -595,8 +728,14 @@ public sealed class Local
     {
         public Internal.TableHandles.TestAutoIncNotInteger TestAutoIncNotInteger => new();
         public Internal.TableHandles.TestDuplicateTableName TestDuplicateTableName => new();
-        public Internal.TableHandles.TestIncompatibleSchedule1 TestIncompatibleSchedule1 => new();
-        public Internal.TableHandles.TestIncompatibleSchedule2 TestIncompatibleSchedule2 => new();
+        public Internal.TableHandles.TestScheduleWithoutPrimaryKey TestScheduleWithoutPrimaryKey =>
+            new();
+        public Internal.TableHandles.TestScheduleWithoutScheduleAt TestScheduleWithoutScheduleAt =>
+            new();
+        public Internal.TableHandles.TestScheduleWithWrongPrimaryKeyType TestScheduleWithWrongPrimaryKeyType =>
+            new();
+        public Internal.TableHandles.TestScheduleWithWrongScheduleAtType TestScheduleWithWrongScheduleAtType =>
+            new();
         public Internal.TableHandles.TestTableTaggedEnum TestTableTaggedEnum => new();
         public Internal.TableHandles.TestUniqueNotEquatable TestUniqueNotEquatable => new();
     }
@@ -616,6 +755,28 @@ public void Invoke(BinaryReader reader, SpacetimeDB.Internal.IReducerContext ctx
         }
     }
 
+    class DummyScheduledReducer : SpacetimeDB.Internal.IReducer
+    {
+        private static readonly TestScheduleIssues.BSATN table = new();
+
+        public SpacetimeDB.Internal.RawReducerDefV9 MakeReducerDef(
+            SpacetimeDB.BSATN.ITypeRegistrar registrar
+        ) =>
+            new(
+                nameof(DummyScheduledReducer),
+                [new(nameof(table), table.GetAlgebraicType(registrar))],
+                null
+            );
+
+        public void Invoke(BinaryReader reader, SpacetimeDB.Internal.IReducerContext ctx)
+        {
+            TestScheduleIssues.DummyScheduledReducer(
+                (SpacetimeDB.ReducerContext)ctx,
+                table.Read(reader)
+            );
+        }
+    }
+
     class OnReducerWithReservedPrefix : SpacetimeDB.Internal.IReducer
     {
         public SpacetimeDB.Internal.RawReducerDefV9 MakeReducerDef(
@@ -664,28 +825,6 @@ public void Invoke(BinaryReader reader, SpacetimeDB.Internal.IReducerContext ctx
         }
     }
 
-    class TestIncompatibleScheduleReducer : SpacetimeDB.Internal.IReducer
-    {
-        private static readonly TestIncompatibleSchedule.BSATN table = new();
-
-        public SpacetimeDB.Internal.RawReducerDefV9 MakeReducerDef(
-            SpacetimeDB.BSATN.ITypeRegistrar registrar
-        ) =>
-            new(
-                nameof(TestIncompatibleScheduleReducer),
-                [new(nameof(table), table.GetAlgebraicType(registrar))],
-                null
-            );
-
-        public void Invoke(BinaryReader reader, SpacetimeDB.Internal.IReducerContext ctx)
-        {
-            TestIncompatibleSchedule.TestIncompatibleScheduleReducer(
-                (SpacetimeDB.ReducerContext)ctx,
-                table.Read(reader)
-            );
-        }
-    }
-
     class TestReducerReturnType : SpacetimeDB.Internal.IReducer
     {
         public SpacetimeDB.Internal.RawReducerDefV9 MakeReducerDef(
@@ -729,11 +868,11 @@ public static void Main()
         );
 
         SpacetimeDB.Internal.Module.RegisterReducer<__ReducerWithReservedPrefix>();
+        SpacetimeDB.Internal.Module.RegisterReducer<DummyScheduledReducer>();
         SpacetimeDB.Internal.Module.RegisterReducer<OnReducerWithReservedPrefix>();
         SpacetimeDB.Internal.Module.RegisterReducer<TestDuplicateReducerKind1>();
         SpacetimeDB.Internal.Module.RegisterReducer<TestDuplicateReducerKind2>();
         SpacetimeDB.Internal.Module.RegisterReducer<TestDuplicateReducerName>();
-        SpacetimeDB.Internal.Module.RegisterReducer<TestIncompatibleScheduleReducer>();
         SpacetimeDB.Internal.Module.RegisterReducer<TestReducerReturnType>();
         SpacetimeDB.Internal.Module.RegisterReducer<TestReducerWithoutContext>();
         SpacetimeDB.Internal.Module.RegisterTable<
@@ -745,12 +884,20 @@ public static void Main()
             SpacetimeDB.Internal.TableHandles.TestDuplicateTableName
         >();
         SpacetimeDB.Internal.Module.RegisterTable<
-            global::TestIncompatibleSchedule,
-            SpacetimeDB.Internal.TableHandles.TestIncompatibleSchedule1
+            global::TestScheduleIssues,
+            SpacetimeDB.Internal.TableHandles.TestScheduleWithoutPrimaryKey
+        >();
+        SpacetimeDB.Internal.Module.RegisterTable<
+            global::TestScheduleIssues,
+            SpacetimeDB.Internal.TableHandles.TestScheduleWithoutScheduleAt
+        >();
+        SpacetimeDB.Internal.Module.RegisterTable<
+            global::TestScheduleIssues,
+            SpacetimeDB.Internal.TableHandles.TestScheduleWithWrongPrimaryKeyType
         >();
         SpacetimeDB.Internal.Module.RegisterTable<
-            global::TestIncompatibleSchedule,
-            SpacetimeDB.Internal.TableHandles.TestIncompatibleSchedule2
+            global::TestScheduleIssues,
+            SpacetimeDB.Internal.TableHandles.TestScheduleWithWrongScheduleAtType
         >();
         SpacetimeDB.Internal.Module.RegisterTable<
             global::TestTableTaggedEnum,
diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#TestIncompatibleSchedule.TestIncompatibleScheduleReducer.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#TestIncompatibleSchedule.TestIncompatibleScheduleReducer.verified.cs
deleted file mode 100644
index 29b0e639c8..0000000000
--- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#TestIncompatibleSchedule.TestIncompatibleScheduleReducer.verified.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-//HintName: TestIncompatibleSchedule.TestIncompatibleScheduleReducer.cs
-// <auto-generated />
-#nullable enable
-
-partial struct TestIncompatibleSchedule
-{
-    [System.Diagnostics.CodeAnalysis.Experimental("STDB_UNSTABLE")]
-    public static void VolatileNonatomicScheduleImmediateTestIncompatibleScheduleReducer(
-        TestIncompatibleSchedule table
-    )
-    {
-        using var stream = new MemoryStream();
-        using var writer = new BinaryWriter(stream);
-        new TestIncompatibleSchedule.BSATN().Write(writer, table);
-        SpacetimeDB.Internal.IReducer.VolatileNonatomicScheduleImmediate(
-            nameof(TestIncompatibleScheduleReducer),
-            stream
-        );
-    }
-} // TestIncompatibleSchedule
diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#TestIncompatibleSchedule.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#TestIncompatibleSchedule.verified.cs
deleted file mode 100644
index 9266a73b19..0000000000
--- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#TestIncompatibleSchedule.verified.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-//HintName: TestIncompatibleSchedule.cs
-// <auto-generated />
-#nullable enable
-
-[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Auto)]
-partial struct TestIncompatibleSchedule : SpacetimeDB.BSATN.IStructuralReadWrite
-{
-    public void ReadFields(System.IO.BinaryReader reader)
-    {
-        ScheduledId = BSATN.ScheduledId.Read(reader);
-        ScheduledAt = BSATN.ScheduledAt.Read(reader);
-    }
-
-    public void WriteFields(System.IO.BinaryWriter writer)
-    {
-        BSATN.ScheduledId.Write(writer, ScheduledId);
-        BSATN.ScheduledAt.Write(writer, ScheduledAt);
-    }
-
-    public readonly partial struct BSATN : SpacetimeDB.BSATN.IReadWrite<TestIncompatibleSchedule>
-    {
-        internal static readonly SpacetimeDB.BSATN.U64 ScheduledId = new();
-        internal static readonly SpacetimeDB.ScheduleAt.BSATN ScheduledAt = new();
-
-        public TestIncompatibleSchedule Read(System.IO.BinaryReader reader) =>
-            SpacetimeDB.BSATN.IStructuralReadWrite.Read<TestIncompatibleSchedule>(reader);
-
-        public void Write(System.IO.BinaryWriter writer, TestIncompatibleSchedule value)
-        {
-            value.WriteFields(writer);
-        }
-
-        public SpacetimeDB.BSATN.AlgebraicType.Ref GetAlgebraicType(
-            SpacetimeDB.BSATN.ITypeRegistrar registrar
-        ) =>
-            registrar.RegisterType<TestIncompatibleSchedule>(
-                _ => new SpacetimeDB.BSATN.AlgebraicType.Product(
-                    new SpacetimeDB.BSATN.AggregateElement[]
-                    {
-                        new(nameof(ScheduledId), ScheduledId.GetAlgebraicType(registrar)),
-                        new(nameof(ScheduledAt), ScheduledAt.GetAlgebraicType(registrar))
-                    }
-                )
-            );
-
-        SpacetimeDB.BSATN.AlgebraicType SpacetimeDB.BSATN.IReadWrite<TestIncompatibleSchedule>.GetAlgebraicType(
-            SpacetimeDB.BSATN.ITypeRegistrar registrar
-        ) => GetAlgebraicType(registrar);
-    }
-
-    public ulong ScheduledId;
-    public SpacetimeDB.ScheduleAt ScheduledAt;
-} // TestIncompatibleSchedule
diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module.verified.txt b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module.verified.txt
index d5844b566b..f68a850679 100644
--- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module.verified.txt
+++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module.verified.txt
@@ -87,31 +87,6 @@ public partial record TestTableTaggedEnum : SpacetimeDB.TaggedEnum<(int X, int Y
     },
     {/*
 
-[SpacetimeDB.Table(
-^^^^^^^^^^^^^^^^^^^
-    Name = "TestIncompatibleSchedule1",
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-    Scheduled = nameof(TestIncompatibleScheduleReducer)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-)]
-^^
-[SpacetimeDB.Table(Name = "TestIncompatibleSchedule2")]
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-public partial struct TestIncompatibleSchedule
-*/
-      Message: Schedule adds extra fields to the row type. Either all `[Table]` attributes should have a `Schedule`, or none of them.,
-      Severity: Error,
-      Descriptor: {
-        Id: STDB0010,
-        Title: Incompatible `[Table(Schedule)]` attributes,
-        MessageFormat: Schedule adds extra fields to the row type. Either all `[Table]` attributes should have a `Schedule`, or none of them.,
-        Category: SpacetimeDB,
-        DefaultSeverity: Error,
-        IsEnabledByDefault: true
-      }
-    },
-    {/*
-
 [SpacetimeDB.Table]
 ^^^^^^^^^^^^^^^^^^^
 [SpacetimeDB.Index]
@@ -153,6 +128,98 @@ public partial struct TestIndexWithEmptyColumns { }
       }
     },
     {/*
+
+[SpacetimeDB.Table(
+ ^^^^^^^^^^^^^^^^^^
+    Name = "TestScheduleWithoutPrimaryKey",
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    Scheduled = "DummyScheduledReducer",
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    ScheduledAt = nameof(ScheduleAtCorrectType)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+)]
+^
+[SpacetimeDB.Table(
+*/
+      Message: TestScheduleWithoutPrimaryKey is a scheduled table but doesn't have a primary key of type `ulong`.,
+      Severity: Error,
+      Descriptor: {
+        Id: STDB0012,
+        Title: Invalid scheduled table declaration,
+        MessageFormat: {0},
+        Category: SpacetimeDB,
+        DefaultSeverity: Error,
+        IsEnabledByDefault: true
+      }
+    },
+    {/*
+)]
+[SpacetimeDB.Table(
+ ^^^^^^^^^^^^^^^^^^
+    Name = "TestScheduleWithWrongPrimaryKeyType",
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    Scheduled = "DummyScheduledReducer",
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    ScheduledAt = nameof(ScheduleAtCorrectType)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+)]
+^
+[SpacetimeDB.Table(Name = "TestScheduleWithoutScheduleAt", Scheduled = "DummyScheduledReducer")]
+*/
+      Message: TestScheduleWithWrongPrimaryKeyType is a scheduled table but doesn't have a primary key of type `ulong`.,
+      Severity: Error,
+      Descriptor: {
+        Id: STDB0012,
+        Title: Invalid scheduled table declaration,
+        MessageFormat: {0},
+        Category: SpacetimeDB,
+        DefaultSeverity: Error,
+        IsEnabledByDefault: true
+      }
+    },
+    {/*
+)]
+[SpacetimeDB.Table(Name = "TestScheduleWithoutScheduleAt", Scheduled = "DummyScheduledReducer")]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+[SpacetimeDB.Table(
+*/
+      Message: Item ScheduledAt not found.,
+      Severity: Error,
+      Descriptor: {
+        Id: STDB0012,
+        Title: Invalid scheduled table declaration,
+        MessageFormat: {0},
+        Category: SpacetimeDB,
+        DefaultSeverity: Error,
+        IsEnabledByDefault: true
+      }
+    },
+    {/*
+[SpacetimeDB.Table(Name = "TestScheduleWithoutScheduleAt", Scheduled = "DummyScheduledReducer")]
+[SpacetimeDB.Table(
+ ^^^^^^^^^^^^^^^^^^
+    Name = "TestScheduleWithWrongScheduleAtType",
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    Scheduled = "DummyScheduledReducer",
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    ScheduledAt = nameof(ScheduleAtWrongType)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+)]
+^
+public partial struct TestScheduleIssues
+*/
+      Message: TestScheduleWithWrongScheduleAtType is a scheduled table but doesn't have a primary key of type `ulong`.,
+      Severity: Error,
+      Descriptor: {
+        Id: STDB0012,
+        Title: Invalid scheduled table declaration,
+        MessageFormat: {0},
+        Category: SpacetimeDB,
+        DefaultSeverity: Error,
+        IsEnabledByDefault: true
+      }
+    },
+    {/*
     [SpacetimeDB.Reducer]
     public static int TestReducerReturnType(ReducerContext ctx) => 0;
                   ^^^
diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/server/Lib.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/server/Lib.cs
index 84b699424a..5938e29383 100644
--- a/crates/bindings-csharp/Codegen.Tests/fixtures/server/Lib.cs
+++ b/crates/bindings-csharp/Codegen.Tests/fixtures/server/Lib.cs
@@ -118,6 +118,10 @@ public static partial class Timers
     [SpacetimeDB.Table(Scheduled = nameof(SendScheduledMessage))]
     public partial struct SendMessageTimer
     {
+        [PrimaryKey]
+        [AutoInc]
+        public ulong ScheduledId;
+        public ScheduleAt ScheduledAt;
         public string Text;
     }
 
diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs
index 06305513a2..97b9b5cfd1 100644
--- a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs
+++ b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs
@@ -747,13 +747,13 @@ static SpacetimeDB.Internal.RawTableDefV9 SpacetimeDB.Internal.ITableView<
                         new global::Timers.SendMessageTimer.BSATN()
                             .GetAlgebraicType(registrar)
                             .Ref_,
-                    PrimaryKey: [1],
+                    PrimaryKey: [0],
                     Indexes:
                     [
                         new(
                             Name: null,
                             AccessorName: "ScheduledId",
-                            Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([1])
+                            Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0])
                         )
                     ],
                     Constraints:
@@ -761,19 +761,19 @@ static SpacetimeDB.Internal.RawTableDefV9 SpacetimeDB.Internal.ITableView<
                         SpacetimeDB.Internal.ITableView<
                             SendMessageTimer,
                             global::Timers.SendMessageTimer
-                        >.MakeUniqueConstraint(1)
+                        >.MakeUniqueConstraint(0)
                     ],
                     Sequences:
                     [
                         SpacetimeDB.Internal.ITableView<
                             SendMessageTimer,
                             global::Timers.SendMessageTimer
-                        >.MakeSequence(1)
+                        >.MakeSequence(0)
                     ],
                     Schedule: SpacetimeDB.Internal.ITableView<
                         SendMessageTimer,
                         global::Timers.SendMessageTimer
-                    >.MakeSchedule("SendScheduledMessage", 2),
+                    >.MakeSchedule("SendScheduledMessage", 1),
                     TableType: SpacetimeDB.Internal.TableType.User,
                     TableAccess: SpacetimeDB.Internal.TableAccess.Private
                 );
diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#Timers.SendMessageTimer.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#Timers.SendMessageTimer.verified.cs
index 5cc70ec992..4ada75d442 100644
--- a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#Timers.SendMessageTimer.verified.cs
+++ b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#Timers.SendMessageTimer.verified.cs
@@ -4,28 +4,27 @@
 
 partial class Timers
 {
-    [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Auto)]
     partial struct SendMessageTimer : SpacetimeDB.BSATN.IStructuralReadWrite
     {
         public void ReadFields(System.IO.BinaryReader reader)
         {
-            Text = BSATN.Text.Read(reader);
             ScheduledId = BSATN.ScheduledId.Read(reader);
             ScheduledAt = BSATN.ScheduledAt.Read(reader);
+            Text = BSATN.Text.Read(reader);
         }
 
         public void WriteFields(System.IO.BinaryWriter writer)
         {
-            BSATN.Text.Write(writer, Text);
             BSATN.ScheduledId.Write(writer, ScheduledId);
             BSATN.ScheduledAt.Write(writer, ScheduledAt);
+            BSATN.Text.Write(writer, Text);
         }
 
         public readonly partial struct BSATN : SpacetimeDB.BSATN.IReadWrite<Timers.SendMessageTimer>
         {
-            internal static readonly SpacetimeDB.BSATN.String Text = new();
             internal static readonly SpacetimeDB.BSATN.U64 ScheduledId = new();
             internal static readonly SpacetimeDB.ScheduleAt.BSATN ScheduledAt = new();
+            internal static readonly SpacetimeDB.BSATN.String Text = new();
 
             public Timers.SendMessageTimer Read(System.IO.BinaryReader reader) =>
                 SpacetimeDB.BSATN.IStructuralReadWrite.Read<Timers.SendMessageTimer>(reader);
@@ -42,9 +41,9 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar
                     _ => new SpacetimeDB.BSATN.AlgebraicType.Product(
                         new SpacetimeDB.BSATN.AggregateElement[]
                         {
-                            new(nameof(Text), Text.GetAlgebraicType(registrar)),
                             new(nameof(ScheduledId), ScheduledId.GetAlgebraicType(registrar)),
-                            new(nameof(ScheduledAt), ScheduledAt.GetAlgebraicType(registrar))
+                            new(nameof(ScheduledAt), ScheduledAt.GetAlgebraicType(registrar)),
+                            new(nameof(Text), Text.GetAlgebraicType(registrar))
                         }
                     )
                 );
@@ -53,8 +52,5 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar
                 SpacetimeDB.BSATN.ITypeRegistrar registrar
             ) => GetAlgebraicType(registrar);
         }
-
-        public ulong ScheduledId;
-        public SpacetimeDB.ScheduleAt ScheduledAt;
     } // SendMessageTimer
 } // Timers
diff --git a/crates/bindings-csharp/Codegen/Diag.cs b/crates/bindings-csharp/Codegen/Diag.cs
index 27c1eaff0a..1635bc0b16 100644
--- a/crates/bindings-csharp/Codegen/Diag.cs
+++ b/crates/bindings-csharp/Codegen/Diag.cs
@@ -105,4 +105,10 @@ IEnumerable<string> fullNames
                 $"Several reducers are assigned to the same lifecycle kind {ctx.kind}: {string.Join(", ", ctx.fullNames)}",
             ctx => Location.None
         );
+
+    public static readonly ErrorDescriptor<(
+        AttributeData attr,
+        string message
+    )> InvalidScheduledDeclaration =
+        new(group, "Invalid scheduled table declaration", ctx => $"{ctx.message}", ctx => ctx.attr);
 }
diff --git a/crates/bindings-csharp/Codegen/Module.cs b/crates/bindings-csharp/Codegen/Module.cs
index a3c44dd1c3..018094c7d0 100644
--- a/crates/bindings-csharp/Codegen/Module.cs
+++ b/crates/bindings-csharp/Codegen/Module.cs
@@ -154,7 +154,7 @@ record TableView
     public readonly bool IsPublic;
     public readonly Scheduled? Scheduled;
 
-    public TableView(TableDeclaration table, AttributeData data)
+    public TableView(TableDeclaration table, AttributeData data, DiagReporter diag)
     {
         var attr = data.ParseAs<TableAttribute>();
 
@@ -162,18 +162,28 @@ public TableView(TableDeclaration table, AttributeData data)
         IsPublic = attr.Public;
         if (attr.Scheduled is { } reducer)
         {
-            Scheduled = new(reducer, table.Members.Select(m => m.Name).IndexOf(attr.ScheduledAt));
-            if (table.GetPrimaryKey(this) is not { } pk || table.Members[pk].Type != "ulong")
+            try
             {
-                throw new InvalidOperationException(
-                    $"{Name} is a scheduled table but doesn't have a primary key of type `ulong`."
+                Scheduled = new(
+                    reducer,
+                    table.Members.Select(m => m.Name).IndexOf(attr.ScheduledAt)
                 );
+                if (table.GetPrimaryKey(this) is not { } pk || table.Members[pk].Type != "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 != "SpacetimeDB.ScheduleAt")
+                {
+                    throw new InvalidOperationException(
+                        $"{Name}.{attr.ScheduledAt} is marked with `ScheduledAt`, but doesn't have the expected type `SpacetimeDB.ScheduleAt`."
+                    );
+                }
             }
-            if (table.Members[Scheduled.ScheduledAtColumn].Type != "SpacetimeDB.ScheduleAt")
+            catch (Exception e)
             {
-                throw new InvalidOperationException(
-                    $"The `{Name}.{attr.ScheduledAt}` column is marked with `ScheduledAt`, but doesn't have the expected type `SpacetimeDB.ScheduleAt`."
-                );
+                diag.Report(ErrorDescriptor.InvalidScheduledDeclaration, (data, e.Message));
             }
         }
     }
@@ -289,7 +299,9 @@ public TableDeclaration(GeneratorAttributeSyntaxContext context, DiagReporter di
             container = container.ContainingType;
         }
 
-        Views = new(context.Attributes.Select(a => new TableView(this, a)).ToImmutableArray());
+        Views = new(
+            context.Attributes.Select(a => new TableView(this, a, diag)).ToImmutableArray()
+        );
         Indexes = new(
             context
                 .TargetSymbol.GetAttributes()
diff --git a/crates/bindings-csharp/Runtime/Attrs.cs b/crates/bindings-csharp/Runtime/Attrs.cs
index 73001a1a49..a3390fb343 100644
--- a/crates/bindings-csharp/Runtime/Attrs.cs
+++ b/crates/bindings-csharp/Runtime/Attrs.cs
@@ -14,7 +14,7 @@ public enum ColumnAttrs : byte
             PrimaryKeyAuto = PrimaryKey | AutoInc,
         }
 
-        [AttributeUsage(AttributeTargets.Field)]
+        [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
         public abstract class ColumnAttribute : Attribute
         {
             public string? Table { get; init; }