diff --git a/src/NHibernate.Test/Ado/BatcherFixture.cs b/src/NHibernate.Test/Ado/BatcherFixture.cs index ff3bfd7aace..d35c8a6c7a1 100644 --- a/src/NHibernate.Test/Ado/BatcherFixture.cs +++ b/src/NHibernate.Test/Ado/BatcherFixture.cs @@ -1,3 +1,4 @@ +using System.Linq; using NHibernate.AdoNet; using NHibernate.Cfg; using NUnit.Framework; @@ -303,5 +304,46 @@ public void AbstractBatcherLogFormattedSql() Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); Cleanup(); } + + [Test] + [Description("The batcher should handle empty batch execution without throwing exceptions.")] + public void EmptyBatchShouldNotThrowException() + { + // This test verifies that batchers handle empty batches correctly + // DbBatchBatcher had a bug where ExecuteBatch was called on an empty batch, + // causing InvalidOperationException: CommandText property has not been initialized + // See GH-3725 + + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + + // Execute queries that don't add to the batch + _ = session.Query().FirstOrDefault(); + + // Prepare a new command which triggers ExecuteBatch on any existing batch + // If the previous command didn't add anything to the batch, this would fail + // before the fix with InvalidOperationException + _ = session.Query().FirstOrDefault(); + + // Test passes if no exception is thrown + transaction.Commit(); + } + + [Test] + [Description("Flush with no pending operations should handle empty batch correctly.")] + public void FlushEmptyBatchShouldNotThrowException() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + + // Query without any modifications + var count = session.Query().Count(); + Assert.That(count, Is.GreaterThanOrEqualTo(0)); + + // Flush with no pending batch operations should not throw + Assert.DoesNotThrow(() => session.Flush()); + + transaction.Commit(); + } } } diff --git a/src/NHibernate.Test/Async/Ado/BatcherFixture.cs b/src/NHibernate.Test/Async/Ado/BatcherFixture.cs index 96146f114a3..592a3aa3666 100644 --- a/src/NHibernate.Test/Async/Ado/BatcherFixture.cs +++ b/src/NHibernate.Test/Async/Ado/BatcherFixture.cs @@ -8,9 +8,11 @@ //------------------------------------------------------------------------------ +using System.Linq; using NHibernate.AdoNet; using NHibernate.Cfg; using NUnit.Framework; +using NHibernate.Linq; namespace NHibernate.Test.Ado { @@ -275,5 +277,46 @@ public async Task AbstractBatcherLogFormattedSqlAsync() Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1)); await (CleanupAsync()); } + + [Test] + [Description("The batcher should handle empty batch execution without throwing exceptions.")] + public async Task EmptyBatchShouldNotThrowExceptionAsync() + { + // This test verifies that batchers handle empty batches correctly + // DbBatchBatcher had a bug where ExecuteBatch was called on an empty batch, + // causing InvalidOperationException: CommandText property has not been initialized + // See GH-3725 + + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + + // Execute queries that don't add to the batch + _ = await (session.Query().FirstOrDefaultAsync()); + + // Prepare a new command which triggers ExecuteBatch on any existing batch + // If the previous command didn't add anything to the batch, this would fail + // before the fix with InvalidOperationException + _ = await (session.Query().FirstOrDefaultAsync()); + + // Test passes if no exception is thrown + await (transaction.CommitAsync()); + } + + [Test] + [Description("Flush with no pending operations should handle empty batch correctly.")] + public async Task FlushEmptyBatchShouldNotThrowExceptionAsync() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + + // Query without any modifications + var count = await (session.Query().CountAsync()); + Assert.That(count, Is.GreaterThanOrEqualTo(0)); + + // Flush with no pending batch operations should not throw + Assert.DoesNotThrowAsync(() => session.FlushAsync()); + + await (transaction.CommitAsync()); + } } } diff --git a/src/NHibernate/AdoNet/DbBatchBatcher.cs b/src/NHibernate/AdoNet/DbBatchBatcher.cs index 4914a107764..748a8aee30d 100644 --- a/src/NHibernate/AdoNet/DbBatchBatcher.cs +++ b/src/NHibernate/AdoNet/DbBatchBatcher.cs @@ -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"); @@ -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");