From 4ba5ff3aeab6d60c28691a2ffe76478b48c209b9 Mon Sep 17 00:00:00 2001 From: Ilya Korol Date: Tue, 31 Mar 2026 19:19:13 +0300 Subject: [PATCH 1/6] IGNITE-30425 Attach TS tracker from initial query request to ongoing result set fetch requests in order to maintain client's observable timestamp up to date Add stricter result set checks and assertions to affected flacky test --- .../handler/ClientInboundMessageHandler.java | 2 +- .../handler/requests/sql/ClientSqlCommon.java | 33 ++- .../sql/ClientSqlCursorNextResultRequest.java | 48 ++-- .../requests/sql/ClientSqlExecuteRequest.java | 7 +- .../jdbc/ItJdbcMultiStatementSelfTest.java | 47 +++- .../ignite/jdbc/util/RowColumnProjection.java | 28 ++ .../jdbc/util/RowsProjectionMatcher.java | 256 ++++++++++++++++++ .../jdbc/util/StatementResultCheck.java | 57 ++++ 8 files changed, 435 insertions(+), 43 deletions(-) create mode 100644 modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowColumnProjection.java create mode 100644 modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java create mode 100644 modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/StatementResultCheck.java diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java index 895872994ea9..d840612c03eb 100644 --- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java +++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java @@ -1119,7 +1119,7 @@ private CompletableFuture processOperation( ); case ClientOp.SQL_CURSOR_NEXT_RESULT_SET: - return ClientSqlCursorNextResultRequest.process(in, resources, partitionOperationsExecutor, metrics); + return ClientSqlCursorNextResultRequest.process(partitionOperationsExecutor, in, resources, metrics, tsTracker); case ClientOp.OPERATION_CANCEL: return ClientOperationCancelRequest.process(in, cancelHandles); diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCommon.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCommon.java index aa8acf537b5a..72adcb2e8c2a 100644 --- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCommon.java +++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCommon.java @@ -36,7 +36,9 @@ import org.apache.ignite.internal.binarytuple.BinaryTupleContainer; import org.apache.ignite.internal.binarytuple.BinaryTupleParser; import org.apache.ignite.internal.client.proto.ClientMessagePacker; +import org.apache.ignite.internal.client.proto.ClientOp; import org.apache.ignite.internal.client.sql.QueryModifier; +import org.apache.ignite.internal.hlc.HybridTimestampTracker; import org.apache.ignite.internal.lang.IgniteInternalCheckedException; import org.apache.ignite.internal.lang.IgniteInternalException; import org.apache.ignite.internal.sql.SqlCommon; @@ -268,6 +270,7 @@ static CompletableFuture writeResultSetAsync( ClientResourceRegistry resources, AsyncResultSetImpl asyncResultSet, ClientHandlerMetricSource metrics, + HybridTimestampTracker parentTsTracker, int pageSize, boolean includePartitionAwarenessMeta, boolean sqlDirectTxMappingSupported, @@ -277,7 +280,7 @@ static CompletableFuture writeResultSetAsync( ) { try { Long nextResultResourceId = sqlMultiStatementSupported && asyncResultSet.cursor().hasNextResult() - ? saveNextResultResource(asyncResultSet.cursor().nextResult(), pageSize, resources, executor) + ? saveNextResultResource(asyncResultSet.cursor().nextResult(), pageSize, resources, parentTsTracker, executor) : null; if ((asyncResultSet.hasRowSet() && asyncResultSet.hasMorePages())) { @@ -317,10 +320,11 @@ private static Long saveNextResultResource( CompletableFuture> nextResultFuture, int pageSize, ClientResourceRegistry resources, + HybridTimestampTracker parentTsTracker, Executor executor ) throws IgniteInternalCheckedException { ClientResource resource = new ClientResource( - new CursorWithPageSize(nextResultFuture, pageSize), + new NextCursorContext(parentTsTracker, pageSize, nextResultFuture), () -> nextResultFuture.thenAccept(cur -> iterateThroughResultsAndCloseThem(cur, executor)) ); @@ -414,22 +418,33 @@ private static void packPartitionAwarenessMeta( } } - /** Holder of the cursor future and page size. */ - static class CursorWithPageSize { + /** Holder of the context for future result set retrieval. */ + static class NextCursorContext { private final CompletableFuture> cursorFuture; private final int pageSize; - - CursorWithPageSize(CompletableFuture> cursorFuture, int pageSize) { - this.cursorFuture = cursorFuture; + private final HybridTimestampTracker parentTsTracker; + + NextCursorContext( + HybridTimestampTracker parentTsTracker, + int pageSize, + CompletableFuture> cursorFuture + ) { + this.parentTsTracker = parentTsTracker; this.pageSize = pageSize; + this.cursorFuture = cursorFuture; } - CompletableFuture> cursorFuture() { - return cursorFuture; + /** Tracker of the request that initiated query processing (i.e. {@link ClientOp#SQL_EXEC}). */ + HybridTimestampTracker parentTsTracker() { + return parentTsTracker; } int pageSize() { return pageSize; } + + CompletableFuture> cursorFuture() { + return cursorFuture; + } } } diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java index 615ad7678cc2..bb084c11c102 100644 --- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java +++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java @@ -23,8 +23,9 @@ import org.apache.ignite.client.handler.ClientResource; import org.apache.ignite.client.handler.ClientResourceRegistry; import org.apache.ignite.client.handler.ResponseWriter; -import org.apache.ignite.client.handler.requests.sql.ClientSqlCommon.CursorWithPageSize; +import org.apache.ignite.client.handler.requests.sql.ClientSqlCommon.NextCursorContext; import org.apache.ignite.internal.client.proto.ClientMessageUnpacker; +import org.apache.ignite.internal.hlc.HybridTimestampTracker; import org.apache.ignite.internal.lang.IgniteInternalCheckedException; import org.apache.ignite.internal.sql.api.AsyncResultSetImpl; import org.apache.ignite.internal.sql.engine.AsyncSqlCursor; @@ -38,42 +39,49 @@ public class ClientSqlCursorNextResultRequest { * Processes the request. * * @param in Unpacker. + * @param requestTsTracker TS tracker attached to request processing * @return Future representing result of operation. */ public static CompletableFuture process( - ClientMessageUnpacker in, + Executor operationExecutor, ClientMessageUnpacker in, ClientResourceRegistry resources, - Executor operationExecutor, - ClientHandlerMetricSource metrics + ClientHandlerMetricSource metrics, + HybridTimestampTracker requestTsTracker ) throws IgniteInternalCheckedException { long resourceId = in.unpackLong(); ClientResource resource = resources.remove(resourceId); - CursorWithPageSize cursorWithPageSize = resource.get(CursorWithPageSize.class); - int pageSize = cursorWithPageSize.pageSize(); + NextCursorContext nextCursorContext = resource.get(NextCursorContext.class); + HybridTimestampTracker parentTsTracker = nextCursorContext.parentTsTracker(); + int pageSize = nextCursorContext.pageSize(); - CompletableFuture f = cursorWithPageSize.cursorFuture() + CompletableFuture f = nextCursorContext.cursorFuture() .thenComposeAsync(cur -> cur.requestNextAsync(pageSize) .thenApply(batchRes -> new AsyncResultSetImpl( cur, batchRes, pageSize ) - ).thenCompose(asyncResultSet -> - ClientSqlCommon.writeResultSetAsync( - resources, - asyncResultSet, - metrics, - pageSize, - false, - false, - true, - false, - operationExecutor) - ).thenApply(rsWriter -> rsWriter), operationExecutor); + ).thenCompose(asyncResultSet -> { + // For multi-statement DML operations, this will help us keep the client's timestamp tracker up to date and + // ensure client reads are consistent with the latest updates. + requestTsTracker.update(parentTsTracker.get()); + + return ClientSqlCommon.writeResultSetAsync( + resources, + asyncResultSet, + metrics, + parentTsTracker, + pageSize, + false, + false, + true, + false, + operationExecutor); + }).thenApply(rsWriter -> rsWriter), operationExecutor); f.whenCompleteAsync((r, t) -> { if (t != null) { - cursorWithPageSize.cursorFuture().thenAccept(cur -> closeRemainingCursors(cur, false, operationExecutor)); + nextCursorContext.cursorFuture().thenAccept(cur -> closeRemainingCursors(cur, false, operationExecutor)); } }, operationExecutor); diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteRequest.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteRequest.java index cbcd37de96b2..7e096f73e9c3 100644 --- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteRequest.java +++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteRequest.java @@ -98,7 +98,7 @@ public static CompletableFuture process( ClockService clockService, NotificationSender notificationSender, @Nullable String username, - boolean sqlMultistatementsSupported, + boolean sqlMultistatementSupported, boolean sqlPartitionAwarenessQualifiedNameSupported, Consumer queryTypeListener ) { @@ -121,7 +121,7 @@ public static CompletableFuture process( resIdHolder ); - ClientSqlProperties props = new ClientSqlProperties(in, sqlMultistatementsSupported); + ClientSqlProperties props = new ClientSqlProperties(in, sqlMultistatementSupported); String statement = in.unpackString(); Object[] arguments = readArgsNotNull(in); @@ -146,10 +146,11 @@ public static CompletableFuture process( resources, asyncResultSet, metrics, + timestampTracker, props.pageSize(), includePartitionAwarenessMeta, sqlDirectTxMappingSupported, - sqlMultistatementsSupported, + sqlMultistatementSupported, sqlPartitionAwarenessQualifiedNameSupported, operationExecutor)) .thenApply(rsWriter -> out -> { diff --git a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcMultiStatementSelfTest.java b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcMultiStatementSelfTest.java index 6cb97f70825e..10da158c14e4 100644 --- a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcMultiStatementSelfTest.java +++ b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcMultiStatementSelfTest.java @@ -19,6 +19,11 @@ import static org.apache.ignite.internal.lang.IgniteStringFormatter.format; import static org.apache.ignite.jdbc.util.JdbcTestUtils.assertThrowsSqlException; +import static org.apache.ignite.jdbc.util.RowsProjectionMatcher.hasValuesInAnyOrder; +import static org.apache.ignite.jdbc.util.RowsProjectionMatcher.hasValuesOrder; +import static org.apache.ignite.jdbc.util.StatementResultCheck.isResultSet; +import static org.apache.ignite.jdbc.util.StatementResultCheck.isUpdateCounter; +import static org.apache.ignite.jdbc.util.StatementResultCheck.noMoreResults; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; @@ -35,9 +40,11 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.util.List; import java.util.concurrent.TimeUnit; import org.apache.ignite.internal.jdbc.JdbcStatement; import org.apache.ignite.internal.sql.SqlCommon; +import org.apache.ignite.jdbc.util.StatementResultCheck; import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -281,18 +288,26 @@ public void statementMustCloseAllDependentCursors() throws SQLException { @Test public void testMixedDmlQueryExecute() throws Exception { - boolean res = stmt.execute("INSERT INTO TEST_TX VALUES (6, 5, '5'); DELETE FROM TEST_TX WHERE ID=6; SELECT 1;"); - assertFalse(res); - assertEquals(1, getResultSetSize()); + assertStatementResults("INSERT INTO TEST_TX VALUES (6, 5, '5'); DELETE FROM TEST_TX WHERE ID=6; SELECT 1;", + isUpdateCounter(1), + isUpdateCounter(1), + isResultSet(rs -> rs.getInt(1), hasValuesOrder(1)), + noMoreResults() + ); - res = stmt.execute("SELECT 1; INSERT INTO TEST_TX VALUES (7, 5, '5'); DELETE FROM TEST_TX WHERE ID=6;"); - assertEquals(true, res); - assertEquals(1, getResultSetSize()); + assertStatementResults("SELECT 1; INSERT INTO TEST_TX VALUES (7, 5, '5'); DELETE FROM TEST_TX WHERE ID=6;", + isResultSet(rs -> rs.getInt(1), hasValuesOrder(1)), + isUpdateCounter(1), + isUpdateCounter(0), + noMoreResults() + ); - // empty results set in the middle - res = stmt.execute("SELECT * FROM TEST_TX; INSERT INTO TEST_TX VALUES (6, 6, '6'); SELECT * FROM TEST_TX;"); - assertEquals(true, res); - assertEquals(11, getResultSetSize()); + assertStatementResults("SELECT * FROM TEST_TX; INSERT INTO TEST_TX VALUES (6, 6, '6'); SELECT * FROM TEST_TX;", + isResultSet(rs -> rs.getInt(1), hasValuesInAnyOrder(1, 2, 3, 4, 7)), + isUpdateCounter(1), + isResultSet(rs -> rs.getInt(1), hasValuesInAnyOrder(1, 2, 3, 4, 6, 7)), + noMoreResults() + ); } @Test @@ -780,6 +795,18 @@ private int getResultSetSize() throws SQLException { return size; } + /** Verifies that after query execution statement returns results that satisfy provided assertions */ + private void assertStatementResults(String query, StatementResultCheck... resultCheck) throws SQLException { + List checks = List.of(resultCheck); + + stmt.execute(query); + + for (StatementResultCheck check : checks) { + check.check(stmt); + stmt.getMoreResults(); + } + } + private boolean checkNoMoreResults() throws SQLException { boolean more = stmt.getMoreResults(); int updCnt = stmt.getUpdateCount(); diff --git a/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowColumnProjection.java b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowColumnProjection.java new file mode 100644 index 000000000000..1eb6295611ea --- /dev/null +++ b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowColumnProjection.java @@ -0,0 +1,28 @@ +package org.apache.ignite.jdbc.util; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Functional interface for extracting a value from the current row of a {@link ResultSet}. + * + * @param Type of the extracted value. + */ +@FunctionalInterface +public interface RowColumnProjection { + /** Extracts a value from the current row of {@code rs}. */ + T extract(ResultSet rs) throws SQLException; + + /** Drains result set to list by projecting each record with provided extractor */ + static List projectRowsColumn(ResultSet rs, RowColumnProjection extractor) throws SQLException { + List result = new ArrayList<>(); + + while (rs.next()) { + result.add(extractor.extract(rs)); + } + + return result; + } +} diff --git a/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java new file mode 100644 index 000000000000..e885e83ab0f5 --- /dev/null +++ b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java @@ -0,0 +1,256 @@ +package org.apache.ignite.jdbc.util; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import org.hamcrest.Description; +import org.hamcrest.SelfDescribing; +import org.hamcrest.TypeSafeMatcher; +import org.jetbrains.annotations.Nullable; + +/** Matcher for result set rows column projection */ +public abstract class RowsProjectionMatcher extends TypeSafeMatcher> { + + private @Nullable MismatchDescriber mismatch; + + @Override + protected final boolean matchesSafely(List projection) { + try { + doMatch(projection); + return true; + + } catch (MismatchException e) { + mismatch = e.describer(); + return false; + } + } + + /** + * Performs the actual matching + * + * @throws MismatchException on first mismatch + */ + abstract void doMatch(List projection) throws MismatchException; + + @Override + protected void describeMismatchSafely(List projection, Description description) { + assert mismatch != null; + description.appendText("Actual projection " + projection + " "); + mismatch.describeTo(description); + } + + /** Matcher verifying that checked projection contains all values from provided list */ + @SafeVarargs + public static RowsProjectionMatcher hasValuesInAnyOrder(T... expectedVals) { + return hasValues("Rows projection with following values in any order ", (projection, expectedProjection) -> { + + Map remaining = new HashMap<>(); + for (T value : expectedProjection) { + remaining.merge(value, 1, Integer::sum); + } + + int expectedSize = expectedProjection.size(); + int actualSize = projection.size(); + + for (int index = 0; index < actualSize; index++) { + + if (index > expectedSize) { + throw MismatchException.tooManyRecords(actualSize, expectedSize, + projection.subList(index, actualSize) + ); + } + + T actual = projection.get(index); + Integer count = remaining.get(actual); + + if (count == null) { + throw MismatchException.unexpectedValue(actual, index, remaining.entrySet().stream() + .flatMap(e -> Collections.nCopies(e.getValue(), e.getKey()).stream()) + .collect(Collectors.toList())); + } + + if (count == 1) { + remaining.remove(actual); + } else { + remaining.put(actual, count - 1); + } + } + + if (!remaining.isEmpty()) { + throw MismatchException.notEnoughRecords(actualSize, expectedSize, + remaining.entrySet().stream() + .flatMap(e -> Collections.nCopies(e.getValue(), e.getKey()).stream()) + .collect(Collectors.toList())); + } + + }, expectedVals); + } + + /** Matcher verifying that checked projection satisfies provided values order */ + @SafeVarargs + public static RowsProjectionMatcher hasValuesOrder(T... expectedVals) { + return hasValues("Rows projection with following values order ", (projection, expectedProjection) -> { + + int expectedSize = expectedProjection.size(); + int actualSize = projection.size(); + + int index; + + for (index = 0; index < actualSize; index++) { + + if (index > expectedSize) { + throw MismatchException.tooManyRecords(actualSize, expectedSize, + projection.subList(index, actualSize) + ); + } + + T actual = projection.get(index); + T expected = expectedProjection.get(index); + + if (!Objects.equals(actual, expected)) { + throw MismatchException.unexpectedValue(actual, index, expected); + } + } + + if (index < expectedSize) { + throw MismatchException.notEnoughRecords(actualSize, expectedSize, + expectedProjection.subList(index, expectedSize)); + } + + }, expectedVals); + } + + @SafeVarargs + private static RowsProjectionMatcher hasValues( + String baseDescription, + ProjectionCheck projectionCheck, + T... expectedVals + ) { + List expectedValues = Arrays.asList(expectedVals); + + return new RowsProjectionMatcher<>() { + + @Override + protected void doMatch(List projection) throws MismatchException { + int expectedSize = expectedValues.size(); + int actualSize = projection.size(); + + if (expectedSize == 0 && actualSize > 0) { + throw MismatchException.shouldHaveBeenEmpty(); + } + + projectionCheck.check(projection, expectedValues); + } + + @Override + public void describeTo(Description description) { + description.appendText(expectedValues.isEmpty() + ? "Empty projection" + : (baseDescription + expectedValues)); + } + }; + } + + + /** Encapsulates a mismatch description for {@link RowsProjectionMatcher}. */ + @FunctionalInterface + private interface MismatchDescriber extends SelfDescribing { + String INDENTATION = " "; + } + + /** Utility interface that encapsulates part of projection check. */ + @FunctionalInterface + private interface ProjectionCheck { + void check(List projection, List expectedProjection) throws MismatchException; + } + + @SuppressWarnings("CheckedExceptionClass") + private static class MismatchException extends Exception { + + private static final long serialVersionUID = 351697710366897072L; + + private final MismatchDescriber describer; + + private MismatchException(MismatchDescriber describer) { + this.describer = describer; + } + + MismatchDescriber describer() { + return describer; + } + + /** + * An unexpected value was encountered while matching in any order. + * + * @param actual The value that was actually extracted. + * @param rowIndex Zero-based row index at which the mismatch occurred. + * @param unmatched Remaining expected values that have not been matched yet. + */ + static MismatchException unexpectedValue(T actual, int rowIndex, List unmatched) { + return new MismatchException(description -> description + .appendText("has unexpected value ") + .appendValue(actual) + .appendText(" at row #" + rowIndex + ";\n") + .appendText(MismatchDescriber.INDENTATION + "Expected values not matched yet: " + unmatched) + ); + } + + /** + * A value at a specific position did not match the expected value (strict-order check). + * + * @param actual The value that was actually extracted. + * @param rowIndex Zero-based row index at which the mismatch occurred. + * @param expected The value that was expected at this position. + */ + static MismatchException unexpectedValue(T actual, int rowIndex, T expected) { + return new MismatchException(description -> description + .appendText("at row #" + rowIndex + " has ") + .appendValue(actual) + .appendText(" while ") + .appendValue(expected) + .appendText(" was expected") + ); + } + + /** + * Projection contains more rows than expected. + * + * @param actualSize Number of rows actually included by projection. + * @param expectedSize Total number of rows that were expected. + * @param redundant The tail of the projection list that wasn't expected. + */ + static MismatchException tooManyRecords(int actualSize, int expectedSize, List redundant) { + return new MismatchException(description -> description + .appendText("has more rows than expected, got " + actualSize + " but " + expectedSize + " was expected;\n") + .appendText(MismatchDescriber.INDENTATION + "Redundant values: " + redundant) + ); + } + + /** + * Projection contains supposed to be empty. + */ + static MismatchException shouldHaveBeenEmpty() { + return new MismatchException(description -> description + .appendText("has values") + ); + } + + /** + * The projection ended before all expected values were seen. + * + * @param actualSize Number of rows actually included by projection. + * @param expectedSize Total number of rows that were expected. + * @param missing The tail of the expected list that was never reached. + */ + static MismatchException notEnoughRecords(int actualSize, int expectedSize, List missing) { + return new MismatchException(description -> description + .appendText("has fewer rows than expected, got " + actualSize + " but " + expectedSize + " was expected;\n") + .appendText(MismatchDescriber.INDENTATION + "Missing values: " + missing) + ); + } + } +} diff --git a/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/StatementResultCheck.java b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/StatementResultCheck.java new file mode 100644 index 000000000000..4c30b85023e3 --- /dev/null +++ b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/StatementResultCheck.java @@ -0,0 +1,57 @@ +package org.apache.ignite.jdbc.util; + +import static org.apache.ignite.jdbc.util.RowColumnProjection.projectRowsColumn; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +@FunctionalInterface +public interface StatementResultCheck { + + void check(Statement statement) throws SQLException; + + default StatementResultCheck and(StatementResultCheck nextCheck) { + return statement -> { + this.check(statement); + nextCheck.check(statement); + }; + } + + static StatementResultCheck noMoreResults() { + return stmt -> { + assertNull(stmt.getResultSet()); + assertEquals(-1, stmt.getUpdateCount()); + }; + } + + static StatementResultCheck isUpdateCounter(int expected) { + return stmt -> { + int updateCounter = stmt.getUpdateCount(); + assertEquals(expected, updateCounter, "Expected update counter equal to " + expected + ", but got " + updateCounter); + }; + } + + static StatementResultCheck isResultSet() { + return StatementResultCheck::assertRs; + } + + static StatementResultCheck isResultSet(RowColumnProjection projection, RowsProjectionMatcher matcher) { + return stmt -> { + ResultSet rs = assertRs(stmt); + assertThat(projectRowsColumn(rs, projection), matcher); + }; + } + + private static ResultSet assertRs(Statement statement) throws SQLException { + ResultSet rs = statement.getResultSet(); + assertNotNull(rs, "Expected next ResultSet, but got "); + + return rs; + } + +} From 3052dbc934fe82550359378e3a79a54d22d2347a Mon Sep 17 00:00:00 2001 From: Ilya Korol Date: Tue, 7 Apr 2026 20:56:31 +0300 Subject: [PATCH 2/6] IGNITE-30425 Checkstyle fixes --- .../jdbc/ItJdbcMultiStatementSelfTest.java | 2 +- .../ignite/jdbc/util/RowColumnProjection.java | 19 +++++++++- .../jdbc/util/RowsProjectionMatcher.java | 27 +++++++++++--- .../jdbc/util/StatementResultCheck.java | 37 +++++++++++++++---- 4 files changed, 71 insertions(+), 14 deletions(-) diff --git a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcMultiStatementSelfTest.java b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcMultiStatementSelfTest.java index 10da158c14e4..f3c0cb7431b9 100644 --- a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcMultiStatementSelfTest.java +++ b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcMultiStatementSelfTest.java @@ -795,7 +795,7 @@ private int getResultSetSize() throws SQLException { return size; } - /** Verifies that after query execution statement returns results that satisfy provided assertions */ + /** Verifies that after query execution statement returns results that satisfy provided assertions. */ private void assertStatementResults(String query, StatementResultCheck... resultCheck) throws SQLException { List checks = List.of(resultCheck); diff --git a/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowColumnProjection.java b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowColumnProjection.java index 1eb6295611ea..f621f6d5f462 100644 --- a/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowColumnProjection.java +++ b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowColumnProjection.java @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + package org.apache.ignite.jdbc.util; import java.sql.ResultSet; @@ -15,7 +32,7 @@ public interface RowColumnProjection { /** Extracts a value from the current row of {@code rs}. */ T extract(ResultSet rs) throws SQLException; - /** Drains result set to list by projecting each record with provided extractor */ + /** Drains result set to list by projecting each record with provided extractor. */ static List projectRowsColumn(ResultSet rs, RowColumnProjection extractor) throws SQLException { List result = new ArrayList<>(); diff --git a/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java index e885e83ab0f5..be44b753ecc1 100644 --- a/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java +++ b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + package org.apache.ignite.jdbc.util; import java.util.Arrays; @@ -12,7 +29,7 @@ import org.hamcrest.TypeSafeMatcher; import org.jetbrains.annotations.Nullable; -/** Matcher for result set rows column projection */ +/** Matcher for result set rows column projection. */ public abstract class RowsProjectionMatcher extends TypeSafeMatcher> { private @Nullable MismatchDescriber mismatch; @@ -30,9 +47,9 @@ protected final boolean matchesSafely(List projection) { } /** - * Performs the actual matching + * Performs the actual matching. * - * @throws MismatchException on first mismatch + * @throws MismatchException on first mismatch. */ abstract void doMatch(List projection) throws MismatchException; @@ -43,7 +60,7 @@ protected void describeMismatchSafely(List projection, Description descriptio mismatch.describeTo(description); } - /** Matcher verifying that checked projection contains all values from provided list */ + /** Matcher verifying that checked projection contains all values from provided list. */ @SafeVarargs public static RowsProjectionMatcher hasValuesInAnyOrder(T... expectedVals) { return hasValues("Rows projection with following values in any order ", (projection, expectedProjection) -> { @@ -90,7 +107,7 @@ public static RowsProjectionMatcher hasValuesInAnyOrder(T... expectedVals }, expectedVals); } - /** Matcher verifying that checked projection satisfies provided values order */ + /** Matcher verifying that checked projection satisfies provided values order. */ @SafeVarargs public static RowsProjectionMatcher hasValuesOrder(T... expectedVals) { return hasValues("Rows projection with following values order ", (projection, expectedProjection) -> { diff --git a/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/StatementResultCheck.java b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/StatementResultCheck.java index 4c30b85023e3..45984b334882 100644 --- a/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/StatementResultCheck.java +++ b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/StatementResultCheck.java @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + package org.apache.ignite.jdbc.util; import static org.apache.ignite.jdbc.util.RowColumnProjection.projectRowsColumn; @@ -10,18 +27,21 @@ import java.sql.SQLException; import java.sql.Statement; +/** + * Utility class for checking statement results after query execution. + */ @FunctionalInterface public interface StatementResultCheck { + /** + * Actual statement check. Call-site is responsible for calling {@link Statement#getMoreResults()} before this method. + * + * @param statement Statement that was used for query execution. + * @throws SQLException If error occur during accessing statement result. + */ void check(Statement statement) throws SQLException; - default StatementResultCheck and(StatementResultCheck nextCheck) { - return statement -> { - this.check(statement); - nextCheck.check(statement); - }; - } - + /** Assert that no more results are retrievable from this statement. */ static StatementResultCheck noMoreResults() { return stmt -> { assertNull(stmt.getResultSet()); @@ -29,6 +49,7 @@ static StatementResultCheck noMoreResults() { }; } + /** Assert that next result is update counter. */ static StatementResultCheck isUpdateCounter(int expected) { return stmt -> { int updateCounter = stmt.getUpdateCount(); @@ -36,10 +57,12 @@ static StatementResultCheck isUpdateCounter(int expected) { }; } + /** Assert that next result is {@link ResultSet}. */ static StatementResultCheck isResultSet() { return StatementResultCheck::assertRs; } + /** Assert that next result is {@link ResultSet} with rows satisfying provided matcher. */ static StatementResultCheck isResultSet(RowColumnProjection projection, RowsProjectionMatcher matcher) { return stmt -> { ResultSet rs = assertRs(stmt); From 1ac55eb33f759854dcddbd8daa3ec164a805224f Mon Sep 17 00:00:00 2001 From: Ilya Korol Date: Wed, 8 Apr 2026 14:17:27 +0300 Subject: [PATCH 3/6] IGNITE-30425 Review fixes --- .../ignite/jdbc/util/RowsProjectionMatcher.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java index be44b753ecc1..8e4faf0980d2 100644 --- a/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java +++ b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java @@ -75,7 +75,7 @@ public static RowsProjectionMatcher hasValuesInAnyOrder(T... expectedVals for (int index = 0; index < actualSize; index++) { - if (index > expectedSize) { + if (index >= expectedSize) { throw MismatchException.tooManyRecords(actualSize, expectedSize, projection.subList(index, actualSize) ); @@ -85,9 +85,10 @@ public static RowsProjectionMatcher hasValuesInAnyOrder(T... expectedVals Integer count = remaining.get(actual); if (count == null) { - throw MismatchException.unexpectedValue(actual, index, remaining.entrySet().stream() - .flatMap(e -> Collections.nCopies(e.getValue(), e.getKey()).stream()) - .collect(Collectors.toList())); + throw MismatchException.unexpectedValue(actual, index, + remaining.entrySet().stream() + .flatMap(e -> Collections.nCopies(e.getValue(), e.getKey()).stream()) + .collect(Collectors.toList())); } if (count == 1) { @@ -119,7 +120,7 @@ public static RowsProjectionMatcher hasValuesOrder(T... expectedVals) { for (index = 0; index < actualSize; index++) { - if (index > expectedSize) { + if (index >= expectedSize) { throw MismatchException.tooManyRecords(actualSize, expectedSize, projection.subList(index, actualSize) ); From 5c3dde044d3646da54ee37991b31ab67f8fcf0b9 Mon Sep 17 00:00:00 2001 From: Ilya Korol Date: Wed, 8 Apr 2026 14:22:21 +0300 Subject: [PATCH 4/6] IGNITE-30425 More review fixes --- .../requests/sql/ClientSqlCursorNextResultRequest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java index bb084c11c102..1a95bd7c95e5 100644 --- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java +++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java @@ -38,8 +38,11 @@ public class ClientSqlCursorNextResultRequest { /** * Processes the request. * + * @param operationExecutor Operation executor. * @param in Unpacker. - * @param requestTsTracker TS tracker attached to request processing + * @param resources Resource bundle. + * @param metrics Client metrics. + * @param requestTsTracker TS tracker attached to current request processing. * @return Future representing result of operation. */ public static CompletableFuture process( From 9ab4bbd66d6344b42fac8a6c0bd11262bd12b2e5 Mon Sep 17 00:00:00 2001 From: Ilya Korol Date: Wed, 8 Apr 2026 14:39:55 +0300 Subject: [PATCH 5/6] IGNITE-30425 More new review fixes --- .../handler/requests/sql/ClientSqlCursorNextResultRequest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java index 1a95bd7c95e5..d80fc38c8293 100644 --- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java +++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java @@ -46,7 +46,8 @@ public class ClientSqlCursorNextResultRequest { * @return Future representing result of operation. */ public static CompletableFuture process( - Executor operationExecutor, ClientMessageUnpacker in, + Executor operationExecutor, + ClientMessageUnpacker in, ClientResourceRegistry resources, ClientHandlerMetricSource metrics, HybridTimestampTracker requestTsTracker From 86578e91311b0a8a9055ba1bf1834ec7b0affe75 Mon Sep 17 00:00:00 2001 From: Ilya Korol Date: Wed, 8 Apr 2026 15:02:47 +0300 Subject: [PATCH 6/6] IGNITE-30425 A bit more review fixes --- .../org/apache/ignite/jdbc/util/RowsProjectionMatcher.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java index 8e4faf0980d2..e63107f6b4e0 100644 --- a/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java +++ b/modules/jdbc/src/testFixtures/java/org/apache/ignite/jdbc/util/RowsProjectionMatcher.java @@ -60,7 +60,7 @@ protected void describeMismatchSafely(List projection, Description descriptio mismatch.describeTo(description); } - /** Matcher verifying that checked projection contains all values from provided list. */ + /** Matcher which ensures that checked projection contains all values from provided list. */ @SafeVarargs public static RowsProjectionMatcher hasValuesInAnyOrder(T... expectedVals) { return hasValues("Rows projection with following values in any order ", (projection, expectedProjection) -> { @@ -108,7 +108,7 @@ public static RowsProjectionMatcher hasValuesInAnyOrder(T... expectedVals }, expectedVals); } - /** Matcher verifying that checked projection satisfies provided values order. */ + /** Matcher which ensures that checked projection contains all values from provided list in same order. */ @SafeVarargs public static RowsProjectionMatcher hasValuesOrder(T... expectedVals) { return hasValues("Rows projection with following values order ", (projection, expectedProjection) -> {