Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="FSharp.Core" Version="9.0.100" />
<PackageVersion Include="FSharp.SystemTextJson" Version="1.3.13" />
<PackageVersion Include="JasperFx" Version="1.21.1" />
<PackageVersion Include="JasperFx.Events" Version="1.24.0" />
<PackageVersion Include="JasperFx.Events.SourceGenerator" Version="1.2.0" />
<PackageVersion Include="JasperFx" Version="1.21.3" />
<PackageVersion Include="JasperFx.Events" Version="1.24.1" />
<PackageVersion Include="JasperFx.Events.SourceGenerator" Version="1.3.0" />
<PackageVersion Include="JasperFx.RuntimeCompiler" Version="4.4.0" />
<PackageVersion Include="Jil" Version="3.0.0-alpha2" />
<PackageVersion Include="Lamar" Version="7.1.1" />
Expand Down Expand Up @@ -60,8 +60,8 @@
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageVersion Include="System.IO.Hashing" Version="10.0.3" />
<PackageVersion Include="Vogen" Version="7.0.0" />
<PackageVersion Include="Weasel.EntityFrameworkCore" Version="8.9.1" />
<PackageVersion Include="Weasel.Postgresql" Version="8.9.1" />
<PackageVersion Include="Weasel.EntityFrameworkCore" Version="8.10.1" />
<PackageVersion Include="Weasel.Postgresql" Version="8.10.1" />
<PackageVersion Include="WolverineFx.Marten" Version="4.2.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
Expand Down
72 changes: 72 additions & 0 deletions src/CoreTests/Bugs/Bug_4224_unique_index_on_mixed_case_table.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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".
/// </summary>
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<TestProjector>(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<Guid>("id").AsPrimaryKey();
Table.AddColumn<Guid>("fake_id");
Table.AddColumn<int>("year");
Table.AddColumn<int>("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<TestEvent>(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; }
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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
/// </summary>
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;
}

/// <summary>
/// An EventProjection with an explicit ApplyAsync override that stores a document type
/// using operations.Store&lt;T&gt;(). 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
/// </summary>
public partial class AuditRecordProjection : EventProjection
{
public override ValueTask ApplyAsync(IDocumentOperations operations, IEvent e, CancellationToken cancellation)
{
switch (e.Data)
{
case AuditableEvent:
operations.Store<AuditRecord>(new AuditRecord
{
Id = Guid.NewGuid(),
StreamId = e.StreamId,
EventType = e.Data.GetType().Name,
Timestamp = e.Timestamp
});
break;
}

return new ValueTask();
}
}

/// <summary>
/// An EventProjection with conventional Create method that returns a document type.
/// The source generator should register this type via the emitted constructor.
/// </summary>
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<T>() 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<AuditRecord>() 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");
}
}
167 changes: 167 additions & 0 deletions src/Marten.EntityFrameworkCore.Tests/EfCoreSchemaTests.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -66,6 +70,40 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
}
}

/// <summary>
/// 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
/// </summary>
public class NoEfSchemaDbContext : DbContext
{
public NoEfSchemaDbContext(DbContextOptions<NoEfSchemaDbContext> options) : base(options)
{
}

public DbSet<Entity> Entities => Set<Entity>();
public DbSet<EntityType> EntityTypes => Set<EntityType>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<EntityType>(entity =>
{
entity.ToTable("entity_type");
entity.HasKey(e => e.Id);
});

modelBuilder.Entity<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]
Expand Down Expand Up @@ -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<SeparateSchemaDbContext>();
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<SeparateSchemaDbContext>(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<string>();
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<NoEfSchemaDbContext>();
});

var extendedObjects = store.Options.Storage.ExtendedSchemaObjects;

var entityTable = extendedObjects.OfType<Table>()
.FirstOrDefault(t => t.Identifier.Name == "entity");
var entityTypeTable = extendedObjects.OfType<Table>()
.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");
}
}
Loading