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
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,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.11.4" />
<PackageVersion Include="Weasel.Postgresql" Version="8.11.4" />
<PackageVersion Include="Weasel.EntityFrameworkCore" Version="8.12.0" />
<PackageVersion Include="Weasel.Postgresql" Version="8.12.0" />
<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
97 changes: 97 additions & 0 deletions src/EventSourcingTests/Bugs/Bug_4224_long_identifier_names.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System;
using System.Threading.Tasks;
using JasperFx.Events;
using JasperFx.Events.Projections;
using Marten;
using Marten.Events;
using Marten.Testing.Harness;
using Shouldly;
using Weasel.Postgresql;
using Xunit;

namespace EventSourcingTests.Bugs;

/// <summary>
/// Regression test for issue #4224: auto-discovered tag types with long names
/// generate FK/PK names exceeding PostgreSQL's 63-char NAMEDATALEN limit.
/// </summary>
public class Bug_4224_long_identifier_names : OneOffConfigurationsContext
{
// This long type name triggers the issue when used as a tag type
public record struct BootstrapTokenResourceName(string Value);
public record BootstrapTokenResourceNameCreated(BootstrapTokenResourceName ResourceName);

public class LongNameAggregate
{
public Guid Id { get; set; }
public string Name { get; set; } = "";

public void Apply(BootstrapTokenResourceNameCreated e)
{
Name = e.ResourceName.Value;
}
}

[Fact]
public async Task can_create_schema_with_long_tag_type_name()
{
StoreOptions(opts =>
{
opts.Events.UseArchivedStreamPartitioning = true;
opts.Events.AddEventType<BootstrapTokenResourceNameCreated>();

// Register the long-named tag type — this would previously fail
// with PostgresqlIdentifierTooLongException
opts.Events.RegisterTagType<BootstrapTokenResourceName>("bootstrap_token_resource_name");
});

// This should NOT throw PostgresqlIdentifierTooLongException
await theStore.Storage.Database.EnsureStorageExistsAsync(typeof(IEvent), default);
await theStore.Storage.ApplyAllConfiguredChangesToDatabaseAsync();
}

[Fact]
public async Task can_create_schema_with_long_tag_type_name_without_archived_partitioning()
{
StoreOptions(opts =>
{
opts.Events.AddEventType<BootstrapTokenResourceNameCreated>();
opts.Events.RegisterTagType<BootstrapTokenResourceName>("bootstrap_token_resource_name");
});

await theStore.Storage.Database.EnsureStorageExistsAsync(typeof(IEvent), default);
await theStore.Storage.ApplyAllConfiguredChangesToDatabaseAsync();
}

[Fact]
public void identifier_shortening_is_deterministic()
{
var name1 = PostgresqlIdentifier.Shorten(
"fkey_mt_event_tag_bootstrap_token_resource_name_seq_id_is_archived");
var name2 = PostgresqlIdentifier.Shorten(
"fkey_mt_event_tag_bootstrap_token_resource_name_seq_id_is_archived");

name1.ShouldBe(name2);
name1.Length.ShouldBeLessThanOrEqualTo(63);
}

[Fact]
public void short_names_are_not_modified()
{
var shortName = "fk_mt_events_stream_id";
PostgresqlIdentifier.Shorten(shortName).ShouldBe(shortName);
}

[Fact]
public void different_long_names_produce_different_short_names()
{
var name1 = PostgresqlIdentifier.Shorten(
"fkey_mt_event_tag_bootstrap_token_resource_name_seq_id_is_archived");
var name2 = PostgresqlIdentifier.Shorten(
"fkey_mt_event_tag_another_very_long_type_name_here_seq_id_is_archived");

name1.ShouldNotBe(name2);
name1.Length.ShouldBeLessThanOrEqualTo(63);
name2.Length.ShouldBeLessThanOrEqualTo(63);
}
}
5 changes: 3 additions & 2 deletions src/Marten/Events/Schema/EventTagTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public EventTagTable(EventGraph events, ITagTypeRegistration registration)
archiving.AsPrimaryKey();
archiving.PartitionByListValues().AddPartition("archived", true);

ForeignKeys.Add(new ForeignKey($"fkey_mt_event_tag_{registration.TableSuffix}_seq_id_is_archived")
ForeignKeys.Add(new ForeignKey(
PostgresqlIdentifier.Shorten($"fkey_mt_event_tag_{registration.TableSuffix}_seq_id_is_archived"))
{
ColumnNames = new[] { "seq_id", "is_archived" },
LinkedNames = new[] { "seq_id", "is_archived" },
Expand All @@ -48,7 +49,7 @@ public EventTagTable(EventGraph events, ITagTypeRegistration registration)
.ForeignKeyTo(new PostgresqlObjectName(events.DatabaseSchemaName, "mt_events"), "seq_id");
}

PrimaryKeyName = $"pk_mt_event_tag_{registration.TableSuffix}";
PrimaryKeyName = PostgresqlIdentifier.Shorten($"pk_mt_event_tag_{registration.TableSuffix}");
}

private static string PostgresqlTypeFor(Type simpleType)
Expand Down
10 changes: 5 additions & 5 deletions src/Marten/Events/Schema/NaturalKeyTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public NaturalKeyTable(EventGraph events, NaturalKeyDefinition naturalKey)
if (isConjoined)
{
// FK must include tenant_id and is_archived to match mt_streams composite PK
ForeignKeys.Add(new ForeignKey($"fk_{Identifier.Name}_stream_tenant_is_archived")
ForeignKeys.Add(new ForeignKey(PostgresqlIdentifier.Shorten($"fk_{Identifier.Name}_stream_tenant_is_archived"))
{
ColumnNames = new[] { streamCol, TenantIdColumn.Name, "is_archived" },
LinkedNames = new[] { "id", TenantIdColumn.Name, "is_archived" },
Expand All @@ -56,7 +56,7 @@ public NaturalKeyTable(EventGraph events, NaturalKeyDefinition naturalKey)
else
{
// FK to mt_streams must include is_archived when streams table is partitioned
ForeignKeys.Add(new ForeignKey($"fk_{Identifier.Name}_stream_is_archived")
ForeignKeys.Add(new ForeignKey(PostgresqlIdentifier.Shorten($"fk_{Identifier.Name}_stream_is_archived"))
{
ColumnNames = new[] { streamCol, "is_archived" },
LinkedNames = new[] { "id", "is_archived" },
Expand All @@ -68,7 +68,7 @@ public NaturalKeyTable(EventGraph events, NaturalKeyDefinition naturalKey)
else if (isConjoined)
{
// FK must include tenant_id to match mt_streams composite PK (tenant_id, id)
ForeignKeys.Add(new ForeignKey($"fk_{Identifier.Name}_stream_tenant")
ForeignKeys.Add(new ForeignKey(PostgresqlIdentifier.Shorten($"fk_{Identifier.Name}_stream_tenant"))
{
ColumnNames = new[] { streamCol, TenantIdColumn.Name },
LinkedNames = new[] { "id", TenantIdColumn.Name },
Expand All @@ -79,7 +79,7 @@ public NaturalKeyTable(EventGraph events, NaturalKeyDefinition naturalKey)
else
{
// FK to mt_streams with CASCADE delete
ForeignKeys.Add(new ForeignKey($"fk_{Identifier.Name}_stream")
ForeignKeys.Add(new ForeignKey(PostgresqlIdentifier.Shorten($"fk_{Identifier.Name}_stream"))
{
ColumnNames = new[] { streamCol },
LinkedNames = new[] { "id" },
Expand All @@ -89,7 +89,7 @@ public NaturalKeyTable(EventGraph events, NaturalKeyDefinition naturalKey)
}

// Index on stream id/key for reverse lookups
Indexes.Add(new IndexDefinition($"idx_{Identifier.Name}_{streamCol}")
Indexes.Add(new IndexDefinition(PostgresqlIdentifier.Shorten($"idx_{Identifier.Name}_{streamCol}"))
{
IsUnique = false,
Columns = new[] { streamCol }
Expand Down
Loading