You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
482 lines
17 KiB
C#
482 lines
17 KiB
C#
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.EntityFrameworkCore.Diagnostics.Internal;
|
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
using EntityFrameworkCore.Jet.Diagnostics.Internal;
|
|
using EntityFrameworkCore.Jet.FunctionalTests.TestUtilities;
|
|
using EntityFrameworkCore.Jet.Infrastructure;
|
|
using EntityFrameworkCore.Jet.Infrastructure.Internal;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Storage;
|
|
using Microsoft.EntityFrameworkCore.TestUtilities;
|
|
using Xunit;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
#nullable disable
|
|
// ReSharper disable UnusedAutoPropertyAccessor.Local
|
|
// ReSharper disable InconsistentNaming
|
|
namespace EntityFrameworkCore.Jet.FunctionalTests
|
|
{
|
|
public class BatchingTest(BatchingTest.BatchingTestFixture fixture)
|
|
: IClassFixture<BatchingTest.BatchingTestFixture>
|
|
{
|
|
protected BatchingTestFixture Fixture { get; } = fixture;
|
|
|
|
[ConditionalTheory]
|
|
[InlineData(true, true, true)]
|
|
[InlineData(false, true, true)]
|
|
[InlineData(true, false, true)]
|
|
[InlineData(false, false, true)]
|
|
[InlineData(true, true, false)]
|
|
[InlineData(false, true, false)]
|
|
[InlineData(true, false, false)]
|
|
[InlineData(false, false, false)]
|
|
public Task Inserts_are_batched_correctly(bool clientPk, bool clientFk, bool clientOrder)
|
|
{
|
|
var expectedBlogs = new List<Blog>();
|
|
return ExecuteWithStrategyInTransactionAsync(
|
|
context =>
|
|
{
|
|
var owner1 = new Owner();
|
|
var owner2 = new Owner();
|
|
context.Owners.Add(owner1);
|
|
context.Owners.Add(owner2);
|
|
|
|
for (var i = 1; i < 500; i++)
|
|
{
|
|
var blog = new Blog();
|
|
if (clientPk)
|
|
{
|
|
blog.Id = Guid.NewGuid();
|
|
}
|
|
|
|
if (clientFk)
|
|
{
|
|
blog.Owner = i % 2 == 0 ? owner1 : owner2;
|
|
}
|
|
|
|
if (clientOrder)
|
|
{
|
|
blog.Order = i;
|
|
}
|
|
|
|
context.Set<Blog>().Add(blog);
|
|
expectedBlogs.Add(blog);
|
|
}
|
|
|
|
return context.SaveChangesAsync();
|
|
},
|
|
context => AssertDatabaseState(context, clientOrder, expectedBlogs));
|
|
}
|
|
|
|
[ConditionalFact]
|
|
public Task Inserts_and_updates_are_batched_correctly()
|
|
{
|
|
var expectedBlogs = new List<Blog>();
|
|
|
|
return ExecuteWithStrategyInTransactionAsync(
|
|
async context =>
|
|
{
|
|
var owner1 = new Owner { Name = "0" };
|
|
var owner2 = new Owner { Name = "1" };
|
|
context.Owners.Add(owner1);
|
|
context.Owners.Add(owner2);
|
|
|
|
var blog1 = new Blog
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Owner = owner1,
|
|
Order = 1
|
|
};
|
|
|
|
context.Set<Blog>().Add(blog1);
|
|
expectedBlogs.Add(blog1);
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
owner2.Name = "2";
|
|
|
|
blog1.Order = 0;
|
|
var blog2 = new Blog
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Owner = owner1,
|
|
Order = 1
|
|
};
|
|
|
|
context.Set<Blog>().Add(blog2);
|
|
expectedBlogs.Add(blog2);
|
|
|
|
var blog3 = new Blog
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Owner = owner2,
|
|
Order = 2
|
|
};
|
|
|
|
context.Set<Blog>().Add(blog3);
|
|
expectedBlogs.Add(blog3);
|
|
|
|
await context.SaveChangesAsync();
|
|
},
|
|
context => AssertDatabaseState(context, true, expectedBlogs));
|
|
}
|
|
|
|
[ConditionalTheory]
|
|
[InlineData(1)]
|
|
[InlineData(3)]
|
|
[InlineData(4)]
|
|
[InlineData(100)]
|
|
public Task Insertion_order_is_preserved(int maxBatchSize)
|
|
{
|
|
var blogId = new Guid();
|
|
|
|
return TestHelpers.ExecuteWithStrategyInTransactionAsync(
|
|
() => (BloggingContext)Fixture.CreateContext(maxBatchSize: maxBatchSize),
|
|
UseTransaction, async context =>
|
|
{
|
|
var owner = new Owner();
|
|
var blog = new Blog { Owner = owner };
|
|
|
|
for (var i = 0; i < 20; i++)
|
|
{
|
|
context.Add(new Post { Order = i, Blog = blog });
|
|
}
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
blogId = blog.Id;
|
|
}, async context =>
|
|
{
|
|
var posts = context.Set<Post>().Where(p => p.BlogId == blogId).OrderBy(p => p.Order);
|
|
var lastId = 0;
|
|
foreach (var post in await posts.ToListAsync())
|
|
{
|
|
Assert.True(post.PostId > lastId, $"Last ID: {lastId}, current ID: {post.PostId}");
|
|
lastId = post.PostId;
|
|
}
|
|
});
|
|
}
|
|
|
|
[ConditionalFact]
|
|
public async Task Deadlock_on_inserts_and_deletes_with_dependents_is_handled_correctly()
|
|
{
|
|
var blogs = new List<Blog>();
|
|
|
|
using (var context = CreateContext())
|
|
{
|
|
var owner1 = new Owner { Name = "0" };
|
|
var owner2 = new Owner { Name = "1" };
|
|
context.Owners.Add(owner1);
|
|
context.Owners.Add(owner2);
|
|
|
|
blogs.Add(
|
|
new Blog
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Owner = owner1,
|
|
Order = 1
|
|
});
|
|
blogs.Add(
|
|
new Blog
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Owner = owner2,
|
|
Order = 2
|
|
});
|
|
blogs.Add(
|
|
new Blog
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Owner = owner1,
|
|
Order = 3
|
|
});
|
|
blogs.Add(
|
|
new Blog
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Owner = owner2,
|
|
Order = 4
|
|
});
|
|
|
|
context.AddRange(blogs);
|
|
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
var tasks = new List<Task>();
|
|
for (var i = 0; i < 10; i++)
|
|
{
|
|
foreach (var blog in blogs)
|
|
{
|
|
tasks.Add(RemoveAndAddPosts(blog));
|
|
}
|
|
}
|
|
|
|
Task.WaitAll(tasks.ToArray());
|
|
|
|
async Task RemoveAndAddPosts(Blog blog)
|
|
{
|
|
using var context = (BloggingContext)Fixture.CreateContext(useConnectionString: true);
|
|
|
|
context.Attach(blog);
|
|
blog.Posts.Clear();
|
|
|
|
blog.Posts.Add(new Post { Comments = { new Comment() } });
|
|
blog.Posts.Add(new Post { Comments = { new Comment() } });
|
|
blog.Posts.Add(new Post { Comments = { new Comment() } });
|
|
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
await Fixture.ReseedAsync();
|
|
}
|
|
|
|
[ConditionalFact]
|
|
public async Task Deadlock_on_deletes_with_dependents_is_handled_correctly()
|
|
{
|
|
var owners = new[] { new Owner { Name = "0" }, new Owner { Name = "1" } };
|
|
using (var context = CreateContext())
|
|
{
|
|
context.Owners.AddRange(owners);
|
|
|
|
for (var h = 0; h <= 40; h++)
|
|
{
|
|
var owner = owners[h % 2];
|
|
var blog = new Blog
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Owner = owner,
|
|
Order = h
|
|
};
|
|
|
|
for (var i = 0; i <= 40; i++)
|
|
{
|
|
blog.Posts.Add(new Post { Comments = { new Comment() } });
|
|
}
|
|
|
|
context.Add(blog);
|
|
}
|
|
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
async Task Action(Owner owner)
|
|
{
|
|
using var context = (BloggingContext)Fixture.CreateContext(useConnectionString: true);
|
|
|
|
context.RemoveRange(await context.Blogs.Where(b => b.OwnerId == owner.Id).ToListAsync());
|
|
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
var tasks = new List<Task>();
|
|
foreach (var owner in owners)
|
|
{
|
|
tasks.Add(Action(owner));
|
|
}
|
|
|
|
Task.WaitAll(tasks.ToArray());
|
|
|
|
using (var context = CreateContext())
|
|
{
|
|
Assert.Empty(await context.Blogs.ToListAsync());
|
|
}
|
|
|
|
await Fixture.ReseedAsync();
|
|
}
|
|
|
|
[ConditionalFact]
|
|
public Task Inserts_when_database_type_is_different()
|
|
=> ExecuteWithStrategyInTransactionAsync(
|
|
context =>
|
|
{
|
|
var owner1 = new Owner { Id = "0", Name = "Zero" };
|
|
var owner2 = new Owner { Id = "A", Name = string.Join("", Enumerable.Repeat('A', 255)) };
|
|
context.Owners.Add(owner1);
|
|
context.Owners.Add(owner2);
|
|
|
|
return context.SaveChangesAsync();
|
|
}, async context => Assert.Equal(2, await context.Owners.CountAsync()));
|
|
|
|
[ConditionalTheory]
|
|
[InlineData(3)]
|
|
[InlineData(4)]
|
|
public Task Inserts_are_batched_only_when_necessary(int minBatchSize)
|
|
{
|
|
var expectedBlogs = new List<Blog>();
|
|
return TestHelpers.ExecuteWithStrategyInTransactionAsync(
|
|
() => (BloggingContext)Fixture.CreateContext(minBatchSize),
|
|
UseTransaction, async context =>
|
|
{
|
|
var owner = new Owner();
|
|
context.Owners.Add(owner);
|
|
|
|
for (var i = 1; i < 3; i++)
|
|
{
|
|
var blog = new Blog { Id = Guid.NewGuid(), Owner = owner };
|
|
|
|
context.Set<Blog>().Add(blog);
|
|
expectedBlogs.Add(blog);
|
|
}
|
|
|
|
Fixture.TestSqlLoggerFactory.Clear();
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
Assert.Contains(
|
|
minBatchSize == 3
|
|
? RelationalResources.LogBatchReadyForExecution(new TestLogger<JetLoggingDefinitions>())
|
|
.GenerateMessage(3)
|
|
: RelationalResources.LogBatchSmallerThanMinBatchSize(new TestLogger<JetLoggingDefinitions>())
|
|
.GenerateMessage(3, 4),
|
|
Fixture.TestSqlLoggerFactory.Log.Select(l => l.Message));
|
|
|
|
Assert.Equal(minBatchSize <= 3 ? 1 : 3, Fixture.TestSqlLoggerFactory.SqlStatements.Count);
|
|
}, context => AssertDatabaseState(context, false, expectedBlogs));
|
|
}
|
|
|
|
private async Task AssertDatabaseState(DbContext context, bool clientOrder, List<Blog> expectedBlogs)
|
|
{
|
|
expectedBlogs = clientOrder
|
|
? [.. expectedBlogs.OrderBy(b => b.Order)]
|
|
: [.. expectedBlogs.OrderBy(b => b.Id)];
|
|
var actualBlogs = clientOrder
|
|
? await context.Set<Blog>().OrderBy(b => b.Order).ToListAsync()
|
|
: [.. expectedBlogs.OrderBy(b => b.Id)];
|
|
Assert.Equal(expectedBlogs.Count, actualBlogs.Count);
|
|
|
|
for (var i = 0; i < actualBlogs.Count; i++)
|
|
{
|
|
var expected = expectedBlogs[i];
|
|
var actual = actualBlogs[i];
|
|
Assert.Equal(expected.Id, actual.Id);
|
|
Assert.Equal(expected.Order, actual.Order);
|
|
Assert.Equal(expected.OwnerId, actual.OwnerId);
|
|
Assert.Equal(expected.Version, actual.Version);
|
|
}
|
|
}
|
|
|
|
private BloggingContext CreateContext() => (BloggingContext)Fixture.CreateContext();
|
|
|
|
private Task ExecuteWithStrategyInTransactionAsync(
|
|
Func<BloggingContext, Task> testOperation,
|
|
Func<BloggingContext, Task> nestedTestOperation)
|
|
=> TestHelpers.ExecuteWithStrategyInTransactionAsync(
|
|
CreateContext, UseTransaction, testOperation, nestedTestOperation);
|
|
|
|
protected void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction)
|
|
=> facade.UseTransaction(transaction.GetDbTransaction());
|
|
|
|
private class BloggingContext(DbContextOptions options) : PoolableDbContext(options)
|
|
{
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<Owner>(
|
|
b =>
|
|
{
|
|
b.Property(e => e.Id).ValueGeneratedOnAdd();
|
|
b.Property(e => e.Version).IsConcurrencyToken().ValueGeneratedOnAddOrUpdate();
|
|
b.Property(e => e.Name).HasColumnType("nvarchar(255)");
|
|
});
|
|
modelBuilder.Entity<Blog>(
|
|
b =>
|
|
{
|
|
b.Property(e => e.Id).HasDefaultValueSql("GenGUID()");
|
|
b.Property(e => e.Version).IsConcurrencyToken().ValueGeneratedOnAddOrUpdate();
|
|
});
|
|
}
|
|
|
|
// ReSharper disable once UnusedMember.Local
|
|
public DbSet<Blog> Blogs { get; set; }
|
|
public DbSet<Owner> Owners { get; set; }
|
|
}
|
|
|
|
private class Blog
|
|
{
|
|
public Guid Id { get; set; }
|
|
public int Order { get; set; }
|
|
public string OwnerId { get; set; }
|
|
public Owner Owner { get; set; }
|
|
public byte[] Version { get; set; }
|
|
public ICollection<Post> Posts { get; } = [];
|
|
}
|
|
|
|
private class Owner
|
|
{
|
|
public string Id { get; set; }
|
|
public string Name { get; set; }
|
|
public byte[] Version { get; set; }
|
|
}
|
|
|
|
private class Post
|
|
{
|
|
public int PostId { get; set; }
|
|
public int? Order { get; set; }
|
|
public Guid BlogId { get; set; }
|
|
public Blog Blog { get; set; }
|
|
public ICollection<Comment> Comments { get; } = [];
|
|
}
|
|
|
|
private class Comment
|
|
{
|
|
public int CommentId { get; set; }
|
|
public int PostId { get; set; }
|
|
public Post Post { get; set; }
|
|
}
|
|
|
|
public class BatchingTestFixture : SharedStoreFixtureBase<PoolableDbContext>
|
|
{
|
|
protected override string StoreName { get; } = "BatchingTest";
|
|
public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory;
|
|
protected override ITestStoreFactory TestStoreFactory => JetTestStoreFactory.Instance;
|
|
protected override Type ContextType { get; } = typeof(BloggingContext);
|
|
|
|
protected override bool ShouldLogCategory(string logCategory)
|
|
=> logCategory == DbLoggerCategory.Update.Name;
|
|
|
|
protected override async Task SeedAsync(PoolableDbContext context)
|
|
{
|
|
await context.Database.EnsureCreatedResilientlyAsync();
|
|
await context.Database.ExecuteSqlRawAsync(
|
|
"""
|
|
|
|
ALTER TABLE Owners
|
|
ALTER COLUMN Name nvarchar(255);
|
|
""");
|
|
}
|
|
|
|
public DbContext CreateContext(int? minBatchSize = null,
|
|
int? maxBatchSize = null,
|
|
bool useConnectionString = false)
|
|
{
|
|
var options = CreateOptions();
|
|
var optionsBuilder = new DbContextOptionsBuilder(options);
|
|
if (useConnectionString)
|
|
{
|
|
RelationalOptionsExtension extension = options.FindExtension<JetOptionsExtension>()
|
|
?? new JetOptionsExtension();
|
|
|
|
extension = extension.WithConnection(null).WithConnectionString(((JetTestStore)TestStore).ConnectionString);
|
|
((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);
|
|
}
|
|
|
|
if (minBatchSize.HasValue)
|
|
{
|
|
new JetDbContextOptionsBuilder(optionsBuilder).MinBatchSize(minBatchSize.Value);
|
|
}
|
|
|
|
if (maxBatchSize.HasValue)
|
|
{
|
|
new JetDbContextOptionsBuilder(optionsBuilder).MinBatchSize(maxBatchSize.Value);
|
|
}
|
|
return new BloggingContext(optionsBuilder.Options);
|
|
}
|
|
}
|
|
}
|
|
}
|