diff --git a/Directory.Packages.props b/Directory.Packages.props
index 00d7fcfd7a..c677d54663 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -13,9 +13,9 @@
-
-
-
+
+
+
@@ -60,8 +60,8 @@
-
-
+
+
diff --git a/src/CoreTests/Bugs/Bug_4224_unique_index_on_mixed_case_table.cs b/src/CoreTests/Bugs/Bug_4224_unique_index_on_mixed_case_table.cs
new file mode 100644
index 0000000000..09516b998d
--- /dev/null
+++ b/src/CoreTests/Bugs/Bug_4224_unique_index_on_mixed_case_table.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Threading.Tasks;
+using JasperFx;
+using JasperFx.Events.Projections;
+using Marten;
+using Marten.Events.Projections.Flattened;
+using Marten.Testing.Harness;
+using Weasel.Core;
+using Weasel.Postgresql.Tables;
+using Xunit;
+
+namespace CoreTests.Bugs;
+
+///
+/// Reproduces https://github.com/JasperFx/weasel/issues/224
+/// A FlatTableProjection with a mixed-case table name and a unique index
+/// should not fail on the second migration run with "relation already exists".
+///
+public class Bug_4224_unique_index_on_mixed_case_table : BugIntegrationContext
+{
+ [Fact]
+ public async Task should_not_fail_on_second_migration_with_unique_index()
+ {
+ // Matches the user's exact scenario from the issue
+ StoreOptions(options =>
+ {
+ options.DatabaseSchemaName = "bug_4224_marten";
+ options.AutoCreateSchemaObjects = AutoCreate.CreateOrUpdate;
+
+ options.Projections.Add(ProjectionLifecycle.Inline);
+ });
+
+ // First migration - creates schema, table, and index
+ await theStore.Storage.ApplyAllConfiguredChangesToDatabaseAsync();
+
+ // Second migration - should detect existing index and not try to recreate it
+ await theStore.Storage.ApplyAllConfiguredChangesToDatabaseAsync();
+ }
+}
+
+public class TestProjector : FlatTableProjection
+{
+ public TestProjector()
+ : base(new DbObjectName("bug_4224", "TestRecords"))
+ {
+ Table.AddColumn("id").AsPrimaryKey();
+ Table.AddColumn("fake_id");
+ Table.AddColumn("year");
+ Table.AddColumn("month");
+
+ Table.Indexes.Add(new IndexDefinition("ix_test_records_unique")
+ {
+ Columns = ["fake_id", "year", "month"],
+ IsUnique = true
+ });
+
+ // Need at least one event handler for validation
+ Project(map =>
+ {
+ map.Map(x => x.FakeId, "fake_id");
+ map.Map(x => x.Year, "year");
+ map.Map(x => x.Month, "month");
+ });
+ }
+}
+
+public class TestEvent
+{
+ public Guid FakeId { get; set; }
+ public int Year { get; set; }
+ public int Month { get; set; }
+}
diff --git a/src/EventSourcingTests/Projections/event_projection_should_register_document_types.cs b/src/EventSourcingTests/Projections/event_projection_should_register_document_types.cs
new file mode 100644
index 0000000000..df6351087a
--- /dev/null
+++ b/src/EventSourcingTests/Projections/event_projection_should_register_document_types.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using JasperFx.Events;
+using JasperFx.Events.Projections;
+using Marten;
+using Marten.Events.Projections;
+using Marten.Testing.Harness;
+using Shouldly;
+using Xunit;
+
+namespace EventSourcingTests.Projections;
+
+///
+/// Document type used in an EventProjection but NOT explicitly registered with Marten.
+/// The source generator should discover this type from Store/Insert calls in ApplyAsync
+/// and register it automatically.
+/// See https://github.com/JasperFx/marten/issues/4166
+///
+public class AuditRecord
+{
+ public Guid Id { get; set; }
+ public Guid StreamId { get; set; }
+ public string EventType { get; set; } = string.Empty;
+ public DateTimeOffset Timestamp { get; set; }
+}
+
+public class AuditableEvent
+{
+ public string Description { get; set; } = string.Empty;
+}
+
+///
+/// An EventProjection with an explicit ApplyAsync override that stores a document type
+/// using operations.Store<T>(). The source generator should detect this and emit a
+/// constructor that registers AuditRecord as a published type.
+/// See https://github.com/JasperFx/marten/issues/4166
+///
+public partial class AuditRecordProjection : EventProjection
+{
+ public override ValueTask ApplyAsync(IDocumentOperations operations, IEvent e, CancellationToken cancellation)
+ {
+ switch (e.Data)
+ {
+ case AuditableEvent:
+ operations.Store(new AuditRecord
+ {
+ Id = Guid.NewGuid(),
+ StreamId = e.StreamId,
+ EventType = e.Data.GetType().Name,
+ Timestamp = e.Timestamp
+ });
+ break;
+ }
+
+ return new ValueTask();
+ }
+}
+
+///
+/// An EventProjection with conventional Create method that returns a document type.
+/// The source generator should register this type via the emitted constructor.
+///
+public partial class AuditRecordCreatorProjection : EventProjection
+{
+ public AuditRecord Create(AuditableEvent e) => new AuditRecord
+ {
+ Id = Guid.NewGuid(),
+ EventType = nameof(AuditableEvent)
+ };
+}
+
+public class event_projection_should_register_document_types : OneOffConfigurationsContext
+{
+ [Fact]
+ public void explicit_apply_async_projection_should_register_document_types()
+ {
+ // Issue #4166: Document types used in operations.Store() inside an explicit
+ // ApplyAsync override should be automatically discovered and registered.
+ StoreOptions(opts =>
+ {
+ opts.Projections.Add(new AuditRecordProjection(), ProjectionLifecycle.Inline);
+ });
+
+ var documentTypes = theStore.StorageFeatures.AllDocumentMappings
+ .Select(x => x.DocumentType).ToList();
+
+ documentTypes.ShouldContain(typeof(AuditRecord),
+ "AuditRecord should be auto-discovered from operations.Store() in ApplyAsync");
+ }
+
+ [Fact]
+ public void conventional_create_projection_should_register_document_types()
+ {
+ // EventProjection with Create method should also register the return type.
+ StoreOptions(opts =>
+ {
+ opts.Projections.Add(new AuditRecordCreatorProjection(), ProjectionLifecycle.Inline);
+ });
+
+ var documentTypes = theStore.StorageFeatures.AllDocumentMappings
+ .Select(x => x.DocumentType).ToList();
+
+ documentTypes.ShouldContain(typeof(AuditRecord),
+ "AuditRecord should be auto-discovered from Create method return type");
+ }
+}
diff --git a/src/Marten.EntityFrameworkCore.Tests/EfCoreSchemaTests.cs b/src/Marten.EntityFrameworkCore.Tests/EfCoreSchemaTests.cs
index 9becfdb2a4..3a55fd4ae2 100644
--- a/src/Marten.EntityFrameworkCore.Tests/EfCoreSchemaTests.cs
+++ b/src/Marten.EntityFrameworkCore.Tests/EfCoreSchemaTests.cs
@@ -1,8 +1,12 @@
using System;
using System.Linq;
+using System.Threading.Tasks;
+using JasperFx;
using Marten.Testing.Harness;
using Microsoft.EntityFrameworkCore;
+using Npgsql;
using Shouldly;
+using Weasel.EntityFrameworkCore;
using Weasel.Postgresql.Tables;
using Xunit;
@@ -66,6 +70,40 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
}
}
+///
+/// DbContext with no explicit schema - these entities should be moved to the
+/// Marten document store schema, including their FK references.
+/// See https://github.com/JasperFx/marten/issues/4192
+///
+public class NoEfSchemaDbContext : DbContext
+{
+ public NoEfSchemaDbContext(DbContextOptions options) : base(options)
+ {
+ }
+
+ public DbSet Entities => Set();
+ public DbSet EntityTypes => Set();
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("entity_type");
+ entity.HasKey(e => e.Id);
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("entity");
+ entity.HasKey(e => e.Id);
+ entity.HasOne(e => e.EntityType)
+ .WithMany()
+ .HasForeignKey(e => e.EntityTypeId)
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+ }
+}
+
public class EfCoreSchemaTests
{
[Fact]
@@ -126,4 +164,133 @@ public void should_move_tables_without_explicit_schema_to_marten_schema()
orderSummariesTable.Identifier.Schema.ShouldBe(martenSchema,
"Tables without explicit schema should be moved to Marten's schema");
}
+
+ [Fact]
+ public void should_return_entity_types_in_fk_dependency_order()
+ {
+ // Issue #4180: GetEntityTypesForMigration should return entity types sorted
+ // so that referenced tables come before referencing tables.
+ var builder = new DbContextOptionsBuilder();
+ builder.UseNpgsql("Host=localhost");
+
+ using var dbContext = new SeparateSchemaDbContext(builder.Options);
+
+ var entityTypes = DbContextExtensions.GetEntityTypesForMigration(dbContext);
+ var names = entityTypes.Select(e => e.GetTableName()).ToList();
+
+ // entity_type must come before entity because entity has a FK to entity_type
+ var entityTypeIndex = names.IndexOf("entity_type");
+ var entityIndex = names.IndexOf("entity");
+
+ entityTypeIndex.ShouldBeGreaterThanOrEqualTo(0);
+ entityIndex.ShouldBeGreaterThanOrEqualTo(0);
+ entityTypeIndex.ShouldBeLessThan(entityIndex,
+ "entity_type should come before entity due to FK dependency (issue #4180)");
+ }
+
+ [Fact]
+ public async Task should_apply_schema_with_fk_dependencies_without_error()
+ {
+ // Issue #4180: End-to-end test proving that Marten can apply schema changes
+ // for EF Core entities with FK dependencies without table ordering errors.
+ const string testSchema = "ef_fk_order_test";
+
+ // Clean up any previous test schema
+ await using var cleanupConn = new NpgsqlConnection(ConnectionSource.ConnectionString);
+ await cleanupConn.OpenAsync();
+ await using (var cmd = cleanupConn.CreateCommand())
+ {
+ cmd.CommandText = $"DROP SCHEMA IF EXISTS {testSchema} CASCADE";
+ await cmd.ExecuteNonQueryAsync();
+ }
+ await cleanupConn.CloseAsync();
+
+ try
+ {
+ await using var store = DocumentStore.For(opts =>
+ {
+ opts.Connection(ConnectionSource.ConnectionString);
+ opts.DatabaseSchemaName = testSchema;
+ opts.AutoCreateSchemaObjects = AutoCreate.All;
+
+ opts.AddEntityTablesFromDbContext(b =>
+ {
+ b.UseNpgsql("Host=localhost");
+ });
+ });
+
+ // This triggers schema creation - should not throw due to FK ordering
+ await store.Storage.ApplyAllConfiguredChangesToDatabaseAsync();
+
+ // Verify tables were created by querying the information schema
+ await using var verifyConn = new NpgsqlConnection(ConnectionSource.ConnectionString);
+ await verifyConn.OpenAsync();
+ await using var verifyCmd = verifyConn.CreateCommand();
+ verifyCmd.CommandText = @"
+ SELECT table_name FROM information_schema.tables
+ WHERE table_schema = @schema
+ ORDER BY table_name";
+ verifyCmd.Parameters.AddWithValue("schema", SeparateSchemaDbContext.EfSchema);
+
+ var tables = new System.Collections.Generic.List();
+ await using var reader = await verifyCmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ tables.Add(reader.GetString(0));
+ }
+
+ tables.ShouldContain("entity");
+ tables.ShouldContain("entity_type");
+ }
+ finally
+ {
+ // Clean up
+ await using var finalConn = new NpgsqlConnection(ConnectionSource.ConnectionString);
+ await finalConn.OpenAsync();
+ await using var finalCmd = finalConn.CreateCommand();
+ finalCmd.CommandText = $"DROP SCHEMA IF EXISTS {testSchema} CASCADE";
+ await finalCmd.ExecuteNonQueryAsync();
+ finalCmd.CommandText = $"DROP SCHEMA IF EXISTS {SeparateSchemaDbContext.EfSchema} CASCADE";
+ await finalCmd.ExecuteNonQueryAsync();
+ }
+ }
+
+ [Fact]
+ public void should_move_tables_and_fk_without_explicit_schema_to_marten_schema()
+ {
+ // Issue #4192: When EF Core entities do NOT have an explicit schema,
+ // tables AND their FK references should be moved to Marten's schema.
+ const string martenSchema = "test_marten_schema";
+
+ using var store = DocumentStore.For(opts =>
+ {
+ opts.Connection(ConnectionSource.ConnectionString);
+ opts.DatabaseSchemaName = martenSchema;
+
+ // NoEfSchemaDbContext does not set an explicit schema
+ opts.AddEntityTablesFromDbContext();
+ });
+
+ var extendedObjects = store.Options.Storage.ExtendedSchemaObjects;
+
+ var entityTable = extendedObjects.OfType()
+ .FirstOrDefault(t => t.Identifier.Name == "entity");
+ var entityTypeTable = extendedObjects.OfType()
+ .FirstOrDefault(t => t.Identifier.Name == "entity_type");
+
+ entityTable.ShouldNotBeNull();
+ entityTypeTable.ShouldNotBeNull();
+
+ // These tables should be moved to Marten's schema
+ entityTable.Identifier.Schema.ShouldBe(martenSchema,
+ "Entity table should be moved to Marten's schema");
+ entityTypeTable.Identifier.Schema.ShouldBe(martenSchema,
+ "EntityType table should be moved to Marten's schema");
+
+ // FK references should also point to Marten's schema
+ var entityFk = entityTable.ForeignKeys.FirstOrDefault();
+ entityFk.ShouldNotBeNull("Entity should have a FK to EntityType");
+ entityFk.LinkedTable!.Schema.ShouldBe(martenSchema,
+ "Foreign keys should also be updated to reference the correct schema");
+ }
}