Skip to content

Commit abc7a9a

Browse files
committed
Fix #3725: DbBatchBatcher empty batch execution bug
Add empty batch validation to DoExecuteBatch() and DoExecuteBatchAsync() to prevent InvalidOperationException when ExecuteBatch is called with no commands. This matches the pattern used in GenericBatchingBatcher.
1 parent e15822d commit abc7a9a

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

src/NHibernate.Test/Ado/BatcherFixture.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Linq;
12
using NHibernate.AdoNet;
23
using NHibernate.Cfg;
34
using NUnit.Framework;
@@ -303,5 +304,46 @@ public void AbstractBatcherLogFormattedSql()
303304
Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1));
304305
Cleanup();
305306
}
307+
308+
[Test]
309+
[Description("The batcher should handle empty batch execution without throwing exceptions.")]
310+
public void EmptyBatchShouldNotThrowException()
311+
{
312+
// This test verifies that batchers handle empty batches correctly
313+
// DbBatchBatcher had a bug where ExecuteBatch was called on an empty batch,
314+
// causing InvalidOperationException: CommandText property has not been initialized
315+
// See GH-3725
316+
317+
using var session = OpenSession();
318+
using var transaction = session.BeginTransaction();
319+
320+
// Execute queries that don't add to the batch
321+
_ = session.Query<VerySimple>().FirstOrDefault();
322+
323+
// Prepare a new command which triggers ExecuteBatch on any existing batch
324+
// If the previous command didn't add anything to the batch, this would fail
325+
// before the fix with InvalidOperationException
326+
_ = session.Query<VerySimple>().FirstOrDefault();
327+
328+
// Test passes if no exception is thrown
329+
transaction.Commit();
330+
}
331+
332+
[Test]
333+
[Description("Flush with no pending operations should handle empty batch correctly.")]
334+
public void FlushEmptyBatchShouldNotThrowException()
335+
{
336+
using var session = OpenSession();
337+
using var transaction = session.BeginTransaction();
338+
339+
// Query without any modifications
340+
var count = session.Query<VerySimple>().Count();
341+
Assert.That(count, Is.GreaterThanOrEqualTo(0));
342+
343+
// Flush with no pending batch operations should not throw
344+
Assert.DoesNotThrow(() => session.Flush());
345+
346+
transaction.Commit();
347+
}
306348
}
307349
}

src/NHibernate.Test/Async/Ado/BatcherFixture.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
//------------------------------------------------------------------------------
99

1010

11+
using System.Linq;
1112
using NHibernate.AdoNet;
1213
using NHibernate.Cfg;
1314
using NUnit.Framework;
15+
using NHibernate.Linq;
1416

1517
namespace NHibernate.Test.Ado
1618
{
@@ -275,5 +277,46 @@ public async Task AbstractBatcherLogFormattedSqlAsync()
275277
Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1));
276278
await (CleanupAsync());
277279
}
280+
281+
[Test]
282+
[Description("The batcher should handle empty batch execution without throwing exceptions.")]
283+
public async Task EmptyBatchShouldNotThrowExceptionAsync()
284+
{
285+
// This test verifies that batchers handle empty batches correctly
286+
// DbBatchBatcher had a bug where ExecuteBatch was called on an empty batch,
287+
// causing InvalidOperationException: CommandText property has not been initialized
288+
// See GH-3725
289+
290+
using var session = OpenSession();
291+
using var transaction = session.BeginTransaction();
292+
293+
// Execute queries that don't add to the batch
294+
_ = await (session.Query<VerySimple>().FirstOrDefaultAsync());
295+
296+
// Prepare a new command which triggers ExecuteBatch on any existing batch
297+
// If the previous command didn't add anything to the batch, this would fail
298+
// before the fix with InvalidOperationException
299+
_ = await (session.Query<VerySimple>().FirstOrDefaultAsync());
300+
301+
// Test passes if no exception is thrown
302+
await (transaction.CommitAsync());
303+
}
304+
305+
[Test]
306+
[Description("Flush with no pending operations should handle empty batch correctly.")]
307+
public async Task FlushEmptyBatchShouldNotThrowExceptionAsync()
308+
{
309+
using var session = OpenSession();
310+
using var transaction = session.BeginTransaction();
311+
312+
// Query without any modifications
313+
var count = await (session.Query<VerySimple>().CountAsync());
314+
Assert.That(count, Is.GreaterThanOrEqualTo(0));
315+
316+
// Flush with no pending batch operations should not throw
317+
Assert.DoesNotThrowAsync(() => session.FlushAsync());
318+
319+
await (transaction.CommitAsync());
320+
}
278321
}
279322
}

src/NHibernate/AdoNet/DbBatchBatcher.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ public override Task AddToBatchAsync(IExpectation expectation, CancellationToken
115115

116116
protected override void DoExecuteBatch(DbCommand ps)
117117
{
118+
if (_currentBatch.BatchCommands.Count == 0)
119+
{
120+
Expectations.VerifyOutcomeBatched(_totalExpectedRowsAffected, 0, ps);
121+
return;
122+
}
123+
118124
try
119125
{
120126
Log.Debug("Executing batch");
@@ -145,6 +151,12 @@ protected override void DoExecuteBatch(DbCommand ps)
145151
protected override async Task DoExecuteBatchAsync(DbCommand ps, CancellationToken cancellationToken)
146152
{
147153
cancellationToken.ThrowIfCancellationRequested();
154+
if (_currentBatch.BatchCommands.Count == 0)
155+
{
156+
Expectations.VerifyOutcomeBatched(_totalExpectedRowsAffected, 0, ps);
157+
return;
158+
}
159+
148160
try
149161
{
150162
Log.Debug("Executing batch");

0 commit comments

Comments
 (0)