Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Making the operation sorting only apply when there are document types… #3114

Merged
merged 1 commit into from
Apr 3, 2024
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
181 changes: 181 additions & 0 deletions src/EventSourcingTests/Bugs/Bug_3113_do_not_reorder_sql_operations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
using Marten.Schema;

namespace EventSourcingTests.Bugs;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Marten;
using Marten.Events;
using Marten.Events.Projections;
using Marten.Testing.Harness;
using Shouldly;
using Weasel.Postgresql.Tables;
using Xunit;

public sealed class Bug_3113_do_not_reorder_sql_operations : BugIntegrationContext
{
[Fact]
public Task does_not_reorder_sql_commands_randomly_single_document_projections()
{
StoreOptions(opts =>
{
opts.Projections.LiveStreamAggregation<Thing>();
opts.Projections.Snapshot<MyProjection1>(SnapshotLifecycle.Inline);
opts.Projections.Add<MyTableProjection>(ProjectionLifecycle.Inline);
});

return run_test();
}

[Fact]
public Task does_not_reorder_sql_commands_randomly_multiple_document_projections()
{
StoreOptions(opts =>
{
opts.Projections.LiveStreamAggregation<Thing>();
opts.Projections.Snapshot<MyProjection1>(SnapshotLifecycle.Inline);
opts.Projections.Snapshot<MyProjection2>(SnapshotLifecycle.Inline);
opts.Projections.Add<MyTableProjection>(ProjectionLifecycle.Inline);
});

return run_test();
}

private async Task run_test()
{
await using var session = theStore.LightweightSession();

var thingId1 = AddThing("First");
var thingId2 = AddThing("Second");

await session.SaveChangesAsync();

var thingUsers1 = AssignUsers(thingId1, 2);
var thingUsers2 = AssignUsers(thingId2, 20);

await session.SaveChangesAsync();

var actualThingUsers1 = await ReadUserIdsAsync(thingId1);
actualThingUsers1.ShouldBe(thingUsers1);

var actualThingUsers2 = await ReadUserIdsAsync(thingId2);
actualThingUsers2.ShouldBe(thingUsers2);

Guid AddThing(string name)
{
var id = Guid.NewGuid();
var created = new ThingCreated(id, name);
session.Events.StartStream<Thing>(created.Id, created);

return id;
}

IEnumerable<Guid> AssignUsers(Guid thingId, int count)
{
var users = Enumerable.Range(1, count).Select(_ => Guid.NewGuid()).ToList();
var assigned = new ThingUsersAssigned(thingId, users);
session.Events.Append(assigned.Id, assigned);

return users;
}

async Task<IReadOnlyList<Guid>> ReadUserIdsAsync(Guid thingId)
{
return await session.QueryAsync<Guid>($"select user_id from {MyTableProjection.UsersTableName} where id = ?", thingId);
}
}

public record ThingCreated(Guid Id, string Name);
public record ThingUsersAssigned(Guid Id, IEnumerable<Guid> UserIds);

public record Thing(Guid Id, string Name)
{
public static Thing Create(ThingCreated @event)
=> new Thing(@event.Id, @event.Name);
}

[DocumentAlias("projection1")]
public record MyProjection1(Guid Id, string Name, IEnumerable<Guid> UserIds)
{
public static MyProjection1 Create(ThingCreated @event)
=> new(@event.Id, @event.Name, []);

public MyProjection1 Apply(ThingUsersAssigned @event)
=> this with
{
UserIds = @event.UserIds
};
}

[DocumentAlias("projection2")]
public record MyProjection2(Guid Id, IEnumerable<Guid> UserIds)
{
public static MyProjection2 Create(ThingCreated @event)
=> new(@event.Id, []);

public MyProjection2 Apply(ThingUsersAssigned @event)
=> this with
{
UserIds = @event.UserIds
};
}

public class MyTableProjection : EventProjection
{
public const string MainTableName = "mt_tbl_bug_3113";
public const string UsersTableName = $"{MainTableName}_users";

public MyTableProjection()
{
var mainTable = new Table(MainTableName);

mainTable.AddColumn<Guid>("id").AsPrimaryKey();
mainTable.AddColumn<string>("name").AsPrimaryKey();

SchemaObjects.Add(mainTable);

var usersTable = new Table(UsersTableName);

usersTable.AddColumn<Guid>("id").AsPrimaryKey();
usersTable.AddColumn<Guid>("user_id").AsPrimaryKey();

SchemaObjects.Add(usersTable);

foreach (var table in SchemaObjects.OfType<Table>())
{
Options.DeleteDataInTableOnTeardown(table.Identifier);
}
}

public void Project(IEvent<ThingCreated> @event, IDocumentOperations ops)
{
ops.QueueSqlCommand(
$"""
insert into {MainTableName} (id, name) values (?, ?)
""",
@event.Data.Id,
@event.Data.Name);
}

public void Project(IEvent<ThingUsersAssigned> @event, IDocumentOperations ops)
{
ops.QueueSqlCommand(
$"""
delete from {UsersTableName} where id = ?
""",
@event.Data.Id);

foreach (var userId in @event.Data.UserIds)
{
ops.QueueSqlCommand(
$"""
insert into {UsersTableName} (id, user_id) values (?, ?)
""",
@event.Data.Id,
userId);
}
}
}
}
20 changes: 13 additions & 7 deletions src/Marten/Internal/UnitOfWork.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using JasperFx.Core;
using JasperFx.Core.Reflection;
using Marten.Events;
Expand Down Expand Up @@ -243,15 +244,20 @@ private bool shouldSort(StoreOptions options, out IComparer<IStorageOperation> c
return false;
}

if (_operations.Where(x => x.Role() != OperationRole.Other).Select(x => x.DocumentType).Distinct().Count(x => x != typeof(StorageFeatures)) == 1)
{
return false;
}

var types = _operations
var rawTypes = _operations
.Where(x => x.Role() != OperationRole.Other)
.Select(x => x.DocumentType)
.Where(x => x != null)
.Distinct()
.Where(x => x != typeof(StorageFeatures))
.Distinct().ToArray();

if (rawTypes.Length <= 1) return false;

var hasRelationship = rawTypes.Any(x => options.Storage.GetTypeDependencies(x).Intersect(rawTypes).Any());

if (!hasRelationship) return false;

var types = rawTypes
.TopologicalSort(type => options.Storage.GetTypeDependencies(type)).ToArray();

if (_operations.OfType<IDeletion>().Any())
Expand Down
Loading