Skip to content

Commit 722f7f9

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 722f7f9

File tree

3 files changed

+94
-0
lines changed

3 files changed

+94
-0
lines changed

src/NHibernate.Test/Ado/BatcherFixture.cs

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

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

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

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)