From e69ebc8b86eaf4320ced32ac7c1a55cde984d402 Mon Sep 17 00:00:00 2001 From: Jean Bisutti Date: Mon, 19 Oct 2026 23:00:32 +0200 Subject: [PATCH] Detect an unbatched statement executed after a JDBC batch --- .../sql/batch/SqlStatementBatchRecorder.java | 9 ++ .../sql/batch/SqlStatementBatchVerifier.java | 13 ++- .../batch/SqlStatementBatchVerifierTest.java | 83 +++++++++++++++++ .../ExpectJdbcBatchingWithBatchSizeTest.java | 89 +++++++++++++++++++ ...xpectJdbcBatchingWithoutBatchSizeTest.java | 35 ++++++++ 5 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 sql/sql-annotations/src/test/java/org/quickperf/sql/batch/SqlStatementBatchVerifierTest.java diff --git a/sql/sql-annotations/src/main/java/org/quickperf/sql/batch/SqlStatementBatchRecorder.java b/sql/sql-annotations/src/main/java/org/quickperf/sql/batch/SqlStatementBatchRecorder.java index dbb94bd5..d54678dc 100644 --- a/sql/sql-annotations/src/main/java/org/quickperf/sql/batch/SqlStatementBatchRecorder.java +++ b/sql/sql-annotations/src/main/java/org/quickperf/sql/batch/SqlStatementBatchRecorder.java @@ -68,6 +68,9 @@ public SqlBatchSizes findRecord(TestExecutionContext testExecutionContext) { @Override public void addQueryExecution(ExecutionInfo execInfo, List queries, int listenerIdentifier) { + if (isEmptyBatch(execInfo)) { + return; + } for (QueryInfo query : queries) { if ( previousStatementsAreBatched && isRequestTypeInsertOrUpdateOrDeleteType(query) @@ -81,6 +84,12 @@ && isRequestTypeInsertOrUpdateOrDeleteType(query) } } + private boolean isEmptyBatch(ExecutionInfo execInfo) { + // An executeBatch() with nothing added sends no SQL, but its batch size is 0, like an unbatched execution. + // It is ignored, so that a recorded 0 batch size always means an unbatched execution + return execInfo.isBatch() && execInfo.getBatchSize() == 0; + } + private int[] createTableWithNewBatchSize(int batchSize) { int newTableLength = differentBatchSizes.length + 1; int[] newTable = new int[newTableLength]; diff --git a/sql/sql-annotations/src/main/java/org/quickperf/sql/batch/SqlStatementBatchVerifier.java b/sql/sql-annotations/src/main/java/org/quickperf/sql/batch/SqlStatementBatchVerifier.java index bff023c1..3672b2f3 100644 --- a/sql/sql-annotations/src/main/java/org/quickperf/sql/batch/SqlStatementBatchVerifier.java +++ b/sql/sql-annotations/src/main/java/org/quickperf/sql/batch/SqlStatementBatchVerifier.java @@ -55,12 +55,21 @@ private PerfIssue verifyThatInsertUpdateDeleteExecutionAreBatched(int[] measured } private int findNumberOfBatchSizeToCheck(int[] measuredBatchSizesAsArray) { - if(measuredBatchSizesAsArray.length == 1) { - return 1; + if(measuredBatchSizesAsArray.length <= 1) { + return measuredBatchSizesAsArray.length; + } + // The last batch can contain fewer statements, but a 0 batch size means that the last execution is not batched + if(lastExecutionIsNotBatched(measuredBatchSizesAsArray)) { + return measuredBatchSizesAsArray.length; } return measuredBatchSizesAsArray.length - 1; } + private boolean lastExecutionIsNotBatched(int[] measuredBatchSizesAsArray) { + int lastBatchSize = measuredBatchSizesAsArray[measuredBatchSizesAsArray.length - 1]; + return lastBatchSize == 0; + } + private PerfIssue verifyBatchSize(int expectedBatchSize, int[] measuredBatchSizesAsArray) { int numberOfBatchSizeToCheck = findNumberOfBatchSizeToCheck(measuredBatchSizesAsArray); diff --git a/sql/sql-annotations/src/test/java/org/quickperf/sql/batch/SqlStatementBatchVerifierTest.java b/sql/sql-annotations/src/test/java/org/quickperf/sql/batch/SqlStatementBatchVerifierTest.java new file mode 100644 index 00000000..8b7dc686 --- /dev/null +++ b/sql/sql-annotations/src/test/java/org/quickperf/sql/batch/SqlStatementBatchVerifierTest.java @@ -0,0 +1,83 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * Copyright 2019-2022 the original author or authors. + */ +package org.quickperf.sql.batch; + +import org.junit.Test; +import org.quickperf.issue.PerfIssue; +import org.quickperf.sql.annotation.ExpectJdbcBatching; +import org.quickperf.sql.annotation.SqlAnnotationBuilder; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SqlStatementBatchVerifierTest { + + private final ExpectJdbcBatching expectJdbcBatchingWithBatchSizeOfThree = SqlAnnotationBuilder.expectJdbcBatching(3); + + @Test public void + should_return_a_perf_issue_if_a_not_batched_execution_follows_a_batch() { + + // GIVEN + SqlBatchSizes measuredBatchSizes = new SqlBatchSizes(new int[] {3, 0}); + + // WHEN + PerfIssue perfIssue = SqlStatementBatchVerifier.INSTANCE.verifyPerfIssue(expectJdbcBatchingWithBatchSizeOfThree, measuredBatchSizes); + + // THEN + assertThat(perfIssue.getDescription()).isEqualTo("Expected batch size <3> but is <0>."); + + } + + @Test public void + should_return_no_perf_issue_if_the_last_batch_is_smaller_than_the_expected_batch_size() { + + // GIVEN + SqlBatchSizes measuredBatchSizes = new SqlBatchSizes(new int[] {3, 2}); + + // WHEN + PerfIssue perfIssue = SqlStatementBatchVerifier.INSTANCE.verifyPerfIssue(expectJdbcBatchingWithBatchSizeOfThree, measuredBatchSizes); + + // THEN + assertThat(perfIssue).isEqualTo(PerfIssue.NONE); + + } + + @Test public void + should_return_no_perf_issue_without_insert_update_and_delete_executions() { + + // GIVEN + SqlBatchSizes measuredBatchSizes = new SqlBatchSizes(new int[0]); + + // WHEN + PerfIssue perfIssue = SqlStatementBatchVerifier.INSTANCE.verifyPerfIssue(expectJdbcBatchingWithBatchSizeOfThree, measuredBatchSizes); + + // THEN + assertThat(perfIssue).isEqualTo(PerfIssue.NONE); + + } + + @Test public void + should_return_no_perf_issue_if_batching_is_expected_to_be_disabled_and_executions_are_not_batched() { + + // GIVEN + ExpectJdbcBatching expectJdbcBatchingWithZeroBatchSize = SqlAnnotationBuilder.expectJdbcBatching(0); + SqlBatchSizes measuredBatchSizes = new SqlBatchSizes(new int[] {0}); + + // WHEN + PerfIssue perfIssue = SqlStatementBatchVerifier.INSTANCE.verifyPerfIssue(expectJdbcBatchingWithZeroBatchSize, measuredBatchSizes); + + // THEN + assertThat(perfIssue).isEqualTo(PerfIssue.NONE); + + } + +} diff --git a/sql/sql-memory-test/src/test/java/ExpectJdbcBatchingWithBatchSizeTest.java b/sql/sql-memory-test/src/test/java/ExpectJdbcBatchingWithBatchSizeTest.java index 32014e1d..41badc5d 100644 --- a/sql/sql-memory-test/src/test/java/ExpectJdbcBatchingWithBatchSizeTest.java +++ b/sql/sql-memory-test/src/test/java/ExpectJdbcBatchingWithBatchSizeTest.java @@ -21,6 +21,9 @@ import org.quickperf.sql.config.MemoryDatabaseHibernateDialect; import javax.persistence.Query; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; import java.util.Properties; import static org.assertj.core.api.Assertions.assertThat; @@ -237,6 +240,92 @@ public void execute_two_insert_query_not_batched() { } + @RunWith(QuickPerfJUnitRunner.class) + public static class AClassHavingAMethodAnnotatedWithExpectJdbcBatchingAndExecutingANotBatchedInsertAfterABatch extends SqlTestBase { + + private static final int BATCH_SIZE = 30; + + @Override + protected Properties getHibernateProperties() { + String hibernateDialect = MemoryDatabaseHibernateDialect.INSTANCE.getHibernateDialect(); + return anHibernateConfig() + .withBatchSize(BATCH_SIZE) + .build(hibernateDialect); + } + + @Test + @ExpectJdbcBatching(batchSize = BATCH_SIZE) + public void execute_a_not_batched_insert_query_after_a_batch() { + + executeInATransaction(entityManager -> { + for (int i = 0; i < BATCH_SIZE; i++) { + Book newBook = new Book(); + newBook.setTitle("new book"); + entityManager.persist(newBook); + } + entityManager.flush(); + + String insertQueryAsString = "INSERT INTO Book (id,title) VALUES (1200, 'Book title')"; + Query insertQuery = entityManager.createNativeQuery(insertQueryAsString); + insertQuery.executeUpdate(); + }); + + } + + } + + @Test public void + should_fail_with_a_not_batched_insert_query_executed_after_a_batch() { + + Class testClass = AClassHavingAMethodAnnotatedWithExpectJdbcBatchingAndExecutingANotBatchedInsertAfterABatch.class; + + PrintableResult printableResult = testResult(testClass); + + assertThat(printableResult.failureCount()).isOne(); + + String testReport = printableResult.toString(); + assertThat(testReport).contains("[PERF] Expected batch size <30> but is <0>."); + + } + + @RunWith(QuickPerfJUnitRunner.class) + public static class AClassHavingAMethodAnnotatedWithExpectJdbcBatchingAndExecutingAnEmptyBatchAfterBatches extends SqlTestBase { + + private static final int BATCH_SIZE = 30; + + @Test + @ExpectJdbcBatching(batchSize = BATCH_SIZE) + public void execute_an_empty_batch_after_batches() throws SQLException { + + try (Connection connection = getConnection(); + PreparedStatement insertStatement = connection.prepareStatement("INSERT INTO Book (id,title) VALUES (?, ?)")) { + for (int i = 1; i <= 2 * BATCH_SIZE; i++) { + insertStatement.setLong(1, 1200 + i); + insertStatement.setString(2, "Book title"); + insertStatement.addBatch(); + if (i % BATCH_SIZE == 0) { + insertStatement.executeBatch(); + } + } + // Executes the remaining statements: none here + insertStatement.executeBatch(); + } + + } + + } + + @Test public void + should_pass_with_an_empty_batch_executed_after_batches() { + + Class testClass = AClassHavingAMethodAnnotatedWithExpectJdbcBatchingAndExecutingAnEmptyBatchAfterBatches.class; + + PrintableResult printableResult = testResult(testClass); + + assertThat(printableResult.failureCount()).isZero(); + + } + @RunWith(QuickPerfJUnitRunner.class) public static class AClassHavingAMethodAnnotatedWithExpectJdbcBatchingAndWithBatchSizeDifferentFromTheExpectedOne extends SqlTestBase { diff --git a/sql/sql-memory-test/src/test/java/ExpectJdbcBatchingWithoutBatchSizeTest.java b/sql/sql-memory-test/src/test/java/ExpectJdbcBatchingWithoutBatchSizeTest.java index dfb38716..82aacfd5 100644 --- a/sql/sql-memory-test/src/test/java/ExpectJdbcBatchingWithoutBatchSizeTest.java +++ b/sql/sql-memory-test/src/test/java/ExpectJdbcBatchingWithoutBatchSizeTest.java @@ -21,6 +21,9 @@ import org.quickperf.sql.config.MemoryDatabaseHibernateDialect; import javax.persistence.Query; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; import java.util.Properties; import static org.assertj.core.api.Assertions.assertThat; @@ -180,4 +183,36 @@ public void execute_one_insert_in_batch_mode() { } + @RunWith(QuickPerfJUnitRunner.class) + public static class AClassHavingAMethodAnnotatedWithExpectJdbcBatchingAndExecutingAnEmptyBatchAfterABatch extends SqlTestBase { + + @Test + @ExpectJdbcBatching() + public void execute_an_empty_batch_after_a_batch() throws SQLException { + + try (Connection connection = getConnection(); + PreparedStatement insertStatement = connection.prepareStatement("INSERT INTO Book (id,title) VALUES (?, ?)")) { + insertStatement.setLong(1, 1200); + insertStatement.setString(2, "Book title"); + insertStatement.addBatch(); + insertStatement.executeBatch(); + + insertStatement.executeBatch(); + } + + } + + } + + @Test public void + should_pass_with_an_empty_batch_executed_after_a_batch() { + + Class testClass = AClassHavingAMethodAnnotatedWithExpectJdbcBatchingAndExecutingAnEmptyBatchAfterABatch.class; + + PrintableResult testResult = testResult(testClass); + + assertThat(testResult.failureCount()).isZero(); + + } + }