Skip to content
Open
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
52 changes: 52 additions & 0 deletions src/NHibernate.Test/Ado/BatcherFixture.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Linq;
using NHibernate.AdoNet;
using NHibernate.Cfg;
using NUnit.Framework;
Expand Down Expand Up @@ -303,5 +304,56 @@ public void AbstractBatcherLogFormattedSql()
Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1));
Cleanup();
}

[Test]
[Description("Inserting exactly BatchSize entities should not throw on commit. See GH-3725.")]
public void InsertExactlyBatchSizeEntitiesShouldNotThrowOnCommit()
{
// This test verifies that DbBatchBatcher handles empty batches correctly.
// The bug (GH-3725): When inserting exactly BatchSize entities, the batch auto-executes
// when full (via ExecuteBatchWithTiming), which clears _currentBatch but NOT _batchCommand.
// On commit, ExecuteBatch() is called, sees _batchCommand is set, and calls DoExecuteBatch
// on an empty _currentBatch, causing InvalidOperationException.

// BatchSize is configured as 10 in this fixture
const int batchSize = 10;

using (var session = OpenSession())
using (var transaction = session.BeginTransaction())
{
// Insert exactly BatchSize entities - this fills the batch and triggers auto-execution
for (int i = 0; i < batchSize; i++)
{
session.Save(new VerySimple { Id = 1000 + i, Name = $"Test{i}", Weight = i * 1.1 });
}

// Commit triggers ExecuteBatch() which would fail on empty batch without the fix
transaction.Commit();
}

Cleanup();
}

[Test]
[Description("Inserting a multiple of BatchSize entities should not throw on commit. See GH-3725.")]
public void InsertMultipleOfBatchSizeEntitiesShouldNotThrowOnCommit()
{
// Same issue as above but with multiple full batches
const int batchSize = 10;
const int multiplier = 3;

using (var session = OpenSession())
using (var transaction = session.BeginTransaction())
{
for (int i = 0; i < batchSize * multiplier; i++)
{
session.Save(new VerySimple { Id = 2000 + i, Name = $"Test{i}", Weight = i * 1.1 });
}

transaction.Commit();
}

Cleanup();
}
}
}
52 changes: 52 additions & 0 deletions src/NHibernate.Test/Async/Ado/BatcherFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//------------------------------------------------------------------------------


using System.Linq;
using NHibernate.AdoNet;
using NHibernate.Cfg;
using NUnit.Framework;
Expand Down Expand Up @@ -275,5 +276,56 @@ public async Task AbstractBatcherLogFormattedSqlAsync()
Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1));
await (CleanupAsync());
}

[Test]
[Description("Inserting exactly BatchSize entities should not throw on commit. See GH-3725.")]
public async Task InsertExactlyBatchSizeEntitiesShouldNotThrowOnCommitAsync()
{
// This test verifies that DbBatchBatcher handles empty batches correctly.
// The bug (GH-3725): When inserting exactly BatchSize entities, the batch auto-executes
// when full (via ExecuteBatchWithTiming), which clears _currentBatch but NOT _batchCommand.
// On commit, ExecuteBatch() is called, sees _batchCommand is set, and calls DoExecuteBatch
// on an empty _currentBatch, causing InvalidOperationException.

// BatchSize is configured as 10 in this fixture
const int batchSize = 10;

using (var session = OpenSession())
using (var transaction = session.BeginTransaction())
{
// Insert exactly BatchSize entities - this fills the batch and triggers auto-execution
for (int i = 0; i < batchSize; i++)
{
await (session.SaveAsync(new VerySimple { Id = 1000 + i, Name = $"Test{i}", Weight = i * 1.1 }));
}

// Commit triggers ExecuteBatch() which would fail on empty batch without the fix
await (transaction.CommitAsync());
}

await (CleanupAsync());
}

[Test]
[Description("Inserting a multiple of BatchSize entities should not throw on commit. See GH-3725.")]
public async Task InsertMultipleOfBatchSizeEntitiesShouldNotThrowOnCommitAsync()
{
// Same issue as above but with multiple full batches
const int batchSize = 10;
const int multiplier = 3;

using (var session = OpenSession())
using (var transaction = session.BeginTransaction())
{
for (int i = 0; i < batchSize * multiplier; i++)
{
await (session.SaveAsync(new VerySimple { Id = 2000 + i, Name = $"Test{i}", Weight = i * 1.1 }));
}

await (transaction.CommitAsync());
}

await (CleanupAsync());
}
}
}
12 changes: 12 additions & 0 deletions src/NHibernate/AdoNet/DbBatchBatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ public override Task AddToBatchAsync(IExpectation expectation, CancellationToken

protected override void DoExecuteBatch(DbCommand ps)
{
if (_currentBatch.BatchCommands.Count == 0)
{
Expectations.VerifyOutcomeBatched(_totalExpectedRowsAffected, 0, ps);
return;
}

try
{
Log.Debug("Executing batch");
Expand Down Expand Up @@ -145,6 +151,12 @@ protected override void DoExecuteBatch(DbCommand ps)
protected override async Task DoExecuteBatchAsync(DbCommand ps, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (_currentBatch.BatchCommands.Count == 0)
{
Expectations.VerifyOutcomeBatched(_totalExpectedRowsAffected, 0, ps);
return;
}

try
{
Log.Debug("Executing batch");
Expand Down