You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by an...@apache.org on 2024/02/06 05:39:27 UTC

(ignite-3) branch main updated: IGNITE-21146 Error handling in Criteria queries (#2989)

This is an automated email from the ASF dual-hosted git repository.

anovikov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new ee00a7c028 IGNITE-21146 Error handling in Criteria queries (#2989)
ee00a7c028 is described below

commit ee00a7c028ef6c8c1d8cdab0169fdd36deb1c5fe
Author: Andrey Novikov <an...@apache.org>
AuthorDate: Tue Feb 6 12:39:22 2024 +0700

    IGNITE-21146 Error handling in Criteria queries (#2989)
---
 .../java/org/apache/ignite/lang/AsyncCursor.java   |   2 +-
 .../{sql => lang}/CursorClosedException.java       |   6 +-
 .../java/org/apache/ignite/lang/ErrorGroups.java   |   9 +-
 .../apache/ignite/sql/async/AsyncResultSet.java    |   4 +-
 .../ignite/table/criteria/CriteriaException.java   | 111 +++++++++++++++++++++
 .../ignite/table/criteria/CriteriaQuerySource.java |   9 +-
 .../ignite/internal/IgniteExceptionArchTest.java   |   2 +-
 .../internal/client/sql/ClientAsyncResultSet.java  |  11 +-
 .../internal/client/table/AbstractClientView.java  |  12 ++-
 .../criteria/CriteriaExceptionMapperUtil.java}     |  31 +++---
 .../internal/table/criteria/CursorAdapter.java     |  18 ++--
 .../table/criteria/QueryCriteriaAsyncCursor.java   |  15 ++-
 .../apache/ignite/internal/util/AsyncCursor.java   |   5 +-
 .../apache/ignite/internal/util/AsyncWrapper.java  |   2 +-
 .../criteria/CriteriaExceptionMapperUtilTest.java  |  80 +++++++++++++++
 .../ignite/internal/util/AsyncWrapperSelfTest.java |   6 +-
 modules/platforms/cpp/ignite/common/error_codes.h  |   3 +-
 modules/platforms/cpp/ignite/odbc/common_types.cpp |   6 +-
 .../platforms/dotnet/Apache.Ignite/ErrorCodes.g.cs |   9 +-
 .../dotnet/Apache.Ignite/Internal/Sql/ResultSet.cs |   2 +-
 .../ignite/internal/sql/api/ItCommonApiTest.java   |   2 +-
 .../ignite/internal/sql/api/ItSqlApiBaseTest.java  |  21 ++--
 .../internal/lang/SqlExceptionMapperUtil.java      |   4 +-
 .../internal/sql/api/AsyncResultSetImpl.java       |  26 ++---
 .../sql/engine/exec/rel/AsyncRootNode.java         |   2 +-
 .../internal/lang/SqlExceptionMapperUtilTest.java  |  23 +++--
 .../ignite/internal/table/ItCriteriaQueryTest.java |  45 ++++++++-
 .../ignite/internal/table/AbstractTableView.java   |  14 ++-
 28 files changed, 362 insertions(+), 118 deletions(-)

diff --git a/modules/api/src/main/java/org/apache/ignite/lang/AsyncCursor.java b/modules/api/src/main/java/org/apache/ignite/lang/AsyncCursor.java
index 8a7e273538..a21b15b961 100644
--- a/modules/api/src/main/java/org/apache/ignite/lang/AsyncCursor.java
+++ b/modules/api/src/main/java/org/apache/ignite/lang/AsyncCursor.java
@@ -49,7 +49,7 @@ public interface AsyncCursor<T> {
      *
      * @return A future which will be completed when next page will be fetched and set as the current page.
      *     The future will return {@code this} for chaining.
-     * @throws IgniteException If resource is closed or if there are no more results.
+     * @throws CursorClosedException If cursor is closed.
      */
     CompletableFuture<? extends AsyncCursor<T>> fetchNextPage();
 
diff --git a/modules/api/src/main/java/org/apache/ignite/sql/CursorClosedException.java b/modules/api/src/main/java/org/apache/ignite/lang/CursorClosedException.java
similarity index 86%
rename from modules/api/src/main/java/org/apache/ignite/sql/CursorClosedException.java
rename to modules/api/src/main/java/org/apache/ignite/lang/CursorClosedException.java
index 2bad187d4f..b7a5053b8c 100644
--- a/modules/api/src/main/java/org/apache/ignite/sql/CursorClosedException.java
+++ b/modules/api/src/main/java/org/apache/ignite/lang/CursorClosedException.java
@@ -15,14 +15,14 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.sql;
+package org.apache.ignite.lang;
 
-import static org.apache.ignite.lang.ErrorGroups.Sql.CURSOR_CLOSED_ERR;
+import static org.apache.ignite.lang.ErrorGroups.Common.CURSOR_CLOSED_ERR;
 
 /**
  * Exception is thrown when a data fetch attempt is performed on a closed cursor.
  */
-public class CursorClosedException extends SqlException {
+public class CursorClosedException extends IgniteException {
     /**
      * Creates an exception instance.
      */
diff --git a/modules/api/src/main/java/org/apache/ignite/lang/ErrorGroups.java b/modules/api/src/main/java/org/apache/ignite/lang/ErrorGroups.java
index e5742d4d0d..01ebc37d01 100755
--- a/modules/api/src/main/java/org/apache/ignite/lang/ErrorGroups.java
+++ b/modules/api/src/main/java/org/apache/ignite/lang/ErrorGroups.java
@@ -133,6 +133,9 @@ public class ErrorGroups {
         /** Operation failed because a node has left the cluster. */
         public static final int NODE_LEFT_ERR = COMMON_ERR_GROUP.registerErrorCode((short) 5);
 
+        /** Cursor is already closed error. */
+        public static final int CURSOR_CLOSED_ERR = COMMON_ERR_GROUP.registerErrorCode((short) 6);
+
         /**
          * This error code represents an internal error caused by faulty logic or coding in the Ignite codebase.
          * In general, this error code should be considered as a non-recoverable error
@@ -211,18 +214,12 @@ public class ErrorGroups {
         /** SQL error group. */
         public static final ErrorGroup SQL_ERR_GROUP = registerGroup("SQL", (short) 4);
 
-        /** No more pages in the cursor error. */
-        public static final int CURSOR_NO_MORE_PAGES_ERR = SQL_ERR_GROUP.registerErrorCode((short) 1);
-
         /** Query without a result set error. */
         public static final int QUERY_NO_RESULT_SET_ERR = SQL_ERR_GROUP.registerErrorCode((short) 2);
 
         /** Schema not found. */
         public static final int SCHEMA_NOT_FOUND_ERR = SQL_ERR_GROUP.registerErrorCode((short) 3);
 
-        /** Cursor is already closed error. */
-        public static final int CURSOR_CLOSED_ERR = SQL_ERR_GROUP.registerErrorCode((short) 4);
-
         /** Statement parsing error. This error is returned when an SQL statement string is not valid according to syntax rules. */
         public static final int STMT_PARSE_ERR = SQL_ERR_GROUP.registerErrorCode((short) 5);
 
diff --git a/modules/api/src/main/java/org/apache/ignite/sql/async/AsyncResultSet.java b/modules/api/src/main/java/org/apache/ignite/sql/async/AsyncResultSet.java
index 4ba91a0822..50a18db1ad 100644
--- a/modules/api/src/main/java/org/apache/ignite/sql/async/AsyncResultSet.java
+++ b/modules/api/src/main/java/org/apache/ignite/sql/async/AsyncResultSet.java
@@ -19,12 +19,11 @@ package org.apache.ignite.sql.async;
 
 import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.lang.AsyncCursor;
-import org.apache.ignite.sql.CursorClosedException;
+import org.apache.ignite.lang.CursorClosedException;
 import org.apache.ignite.sql.NoRowSetExpectedException;
 import org.apache.ignite.sql.ResultSet;
 import org.apache.ignite.sql.ResultSetMetadata;
 import org.apache.ignite.sql.Session;
-import org.apache.ignite.sql.SqlException;
 import org.apache.ignite.sql.SqlRow;
 import org.apache.ignite.table.mapper.Mapper;
 import org.apache.ignite.tx.Transaction;
@@ -131,7 +130,6 @@ public interface AsyncResultSet<T> extends AsyncCursor<T> {
      *     The future will return {@code this} for chaining.
      * @throws NoRowSetExpectedException If no row set is expected as a query result.
      * @throws CursorClosedException If cursor is closed.
-     * @throws SqlException If there are no more pages.
      */
     @Override
     CompletableFuture<? extends AsyncResultSet<T>> fetchNextPage();
diff --git a/modules/api/src/main/java/org/apache/ignite/table/criteria/CriteriaException.java b/modules/api/src/main/java/org/apache/ignite/table/criteria/CriteriaException.java
new file mode 100644
index 0000000000..c51b247bce
--- /dev/null
+++ b/modules/api/src/main/java/org/apache/ignite/table/criteria/CriteriaException.java
@@ -0,0 +1,111 @@
+/*
+ * 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.table.criteria;
+
+import java.util.UUID;
+import org.apache.ignite.lang.IgniteException;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Criteria exception base class.
+ */
+public class CriteriaException extends IgniteException {
+    /**
+     * Creates an exception with the given error code.
+     *
+     * @param code Full error code.
+     */
+    public CriteriaException(int code) {
+        super(code);
+    }
+
+    /**
+     * Creates an exception with the given trace ID and error code.
+     *
+     * @param traceId Unique identifier of the exception.
+     * @param code Full error code.
+     */
+    public CriteriaException(UUID traceId, int code) {
+        super(traceId, code);
+    }
+
+    /**
+     * Creates an exception with the given error code and detailed message.
+     *
+     * @param code Full error code.
+     * @param message Detailed message.
+     */
+    public CriteriaException(int code, String message) {
+        super(code, message);
+    }
+
+    /**
+     * Creates an exception with the given trace ID, error code, and detailed message.
+     *
+     * @param traceId Unique identifier of this exception.
+     * @param code Full error code.
+     * @param message Detailed message.
+     */
+    public CriteriaException(UUID traceId, int code, String message) {
+        super(traceId, code, message);
+    }
+
+    /**
+     * Creates an exception with the given error code and cause.
+     *
+     * @param code Full error code.
+     * @param cause Optional nested exception (can be {@code null}).
+     */
+    public CriteriaException(int code, Throwable cause) {
+        super(code, cause);
+    }
+
+    /**
+     * Creates an exception with the given trace ID, error code, and cause.
+     *
+     * @param traceId Unique identifier of the exception.
+     * @param code Full error code.
+     * @param cause Optional nested exception (can be {@code null}).
+     */
+    public CriteriaException(UUID traceId, int code, Throwable cause) {
+        super(traceId, code, cause);
+    }
+
+    /**
+     * Creates an exception with the given error code, detailed message, and cause.
+     *
+     * @param code Full error code.
+     * @param message Detailed message.
+     * @param cause Optional nested exception (can be {@code null}).
+     */
+    public CriteriaException(int code, String message, @Nullable Throwable cause) {
+        super(code, message, cause);
+    }
+
+    /**
+     * Creates an exception with the given trace ID, error code, detailed message, and cause.
+     *
+     * @param traceId Unique identifier of the exception.
+     * @param code Full error code.
+     * @param message Detailed message.
+     * @param cause Optional nested exception (can be {@code null}).
+     */
+    public CriteriaException(UUID traceId, int code, String message, @Nullable Throwable cause) {
+        super(traceId, code, message, cause);
+    }
+}
diff --git a/modules/api/src/main/java/org/apache/ignite/table/criteria/CriteriaQuerySource.java b/modules/api/src/main/java/org/apache/ignite/table/criteria/CriteriaQuerySource.java
index bfcf9fd3cc..8d10452c85 100644
--- a/modules/api/src/main/java/org/apache/ignite/table/criteria/CriteriaQuerySource.java
+++ b/modules/api/src/main/java/org/apache/ignite/table/criteria/CriteriaQuerySource.java
@@ -20,7 +20,6 @@ package org.apache.ignite.table.criteria;
 import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.lang.AsyncCursor;
 import org.apache.ignite.lang.Cursor;
-import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.tx.Transaction;
 import org.jetbrains.annotations.Nullable;
 
@@ -36,7 +35,7 @@ public interface CriteriaQuerySource<T> {
      * @param tx Transaction to execute the query within or {@code null} to run within implicit transaction.
      * @param criteria The predicate to filter entries or {@code null} to return all entries from the underlying table.
      * @return Iterator with query results.
-     * @throws IgniteException If failed.
+     * @throws CriteriaException If failed.
      */
     default Cursor<T> query(@Nullable Transaction tx, @Nullable Criteria criteria) {
         return query(tx, criteria, null);
@@ -49,7 +48,7 @@ public interface CriteriaQuerySource<T> {
      * @param criteria The predicate to filter entries or {@code null} to return all entries from the underlying table.
      * @param opts Criteria query options or {@code null} to use default.
      * @return Iterator with query results.
-     * @throws IgniteException If failed.
+     * @throws CriteriaException If failed.
      */
     Cursor<T> query(@Nullable Transaction tx, @Nullable Criteria criteria, @Nullable CriteriaQueryOptions opts);
 
@@ -59,7 +58,7 @@ public interface CriteriaQuerySource<T> {
      * @param tx Transaction to execute the query within or {@code null} to run within implicit transaction.
      * @param criteria The predicate to filter entries or {@code null} to return all entries from the underlying table.
      * @return Future that represents the pending completion of the operation.
-     * @throws IgniteException If failed.
+     * @throws CriteriaException If failed.
      */
     default CompletableFuture<AsyncCursor<T>> queryAsync(@Nullable Transaction tx, @Nullable Criteria criteria) {
         return queryAsync(tx, criteria, null);
@@ -72,7 +71,7 @@ public interface CriteriaQuerySource<T> {
      * @param criteria The predicate to filter entries or {@code null} to return all entries from the underlying table.
      * @param opts Criteria query options or {@code null} to use default.
      * @return Future that represents the pending completion of the operation.
-     * @throws IgniteException If failed.
+     * @throws CriteriaException If failed.
      */
     CompletableFuture<AsyncCursor<T>> queryAsync(
             @Nullable Transaction tx,
diff --git a/modules/arch-test/src/test/java/org/apache/ignite/internal/IgniteExceptionArchTest.java b/modules/arch-test/src/test/java/org/apache/ignite/internal/IgniteExceptionArchTest.java
index 6cdcb2727c..eca5e80bb1 100644
--- a/modules/arch-test/src/test/java/org/apache/ignite/internal/IgniteExceptionArchTest.java
+++ b/modules/arch-test/src/test/java/org/apache/ignite/internal/IgniteExceptionArchTest.java
@@ -40,12 +40,12 @@ import org.apache.ignite.client.IgniteClientConnectionException;
 import org.apache.ignite.client.IgniteClientFeatureNotSupportedByServerException;
 import org.apache.ignite.internal.network.RecipientLeftException;
 import org.apache.ignite.internal.network.UnresolvableConsistentIdException;
+import org.apache.ignite.lang.CursorClosedException;
 import org.apache.ignite.lang.IgniteCheckedException;
 import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.lang.LocationProvider.RootLocationProvider;
 import org.apache.ignite.security.exception.InvalidCredentialsException;
 import org.apache.ignite.security.exception.UnsupportedAuthenticationTypeException;
-import org.apache.ignite.sql.CursorClosedException;
 import org.apache.ignite.sql.NoRowSetExpectedException;
 
 /**
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientAsyncResultSet.java b/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientAsyncResultSet.java
index 1df14b84cf..51cf656195 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientAsyncResultSet.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientAsyncResultSet.java
@@ -18,7 +18,6 @@
 package org.apache.ignite.internal.client.sql;
 
 import static org.apache.ignite.internal.util.CompletableFutures.nullCompletedFuture;
-import static org.apache.ignite.lang.ErrorGroups.Sql.CURSOR_NO_MORE_PAGES_ERR;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -34,13 +33,12 @@ import org.apache.ignite.internal.client.table.ClientSchema;
 import org.apache.ignite.internal.marshaller.ClientMarshallerReader;
 import org.apache.ignite.internal.marshaller.Marshaller;
 import org.apache.ignite.internal.marshaller.MarshallerException;
+import org.apache.ignite.lang.CursorClosedException;
 import org.apache.ignite.lang.ErrorGroups.Client;
 import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.sql.ColumnMetadata;
-import org.apache.ignite.sql.CursorClosedException;
 import org.apache.ignite.sql.NoRowSetExpectedException;
 import org.apache.ignite.sql.ResultSetMetadata;
-import org.apache.ignite.sql.SqlException;
 import org.apache.ignite.sql.SqlRow;
 import org.apache.ignite.sql.async.AsyncResultSet;
 import org.apache.ignite.table.mapper.Mapper;
@@ -158,15 +156,10 @@ class ClientAsyncResultSet<T> implements AsyncResultSet<T> {
     public CompletableFuture<? extends AsyncResultSet<T>> fetchNextPage() {
         requireResultSet();
 
-        if (closed) {
+        if (closed || !hasMorePages()) {
             return CompletableFuture.failedFuture(new CursorClosedException());
         }
 
-        if (!hasMorePages()) {
-            return CompletableFuture.failedFuture(
-                    new SqlException(CURSOR_NO_MORE_PAGES_ERR, "There are no more pages."));
-        }
-
         return ch.serviceAsync(
                 ClientOp.SQL_CURSOR_NEXT_PAGE,
                 w -> w.out().packLong(resourceId),
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/table/AbstractClientView.java b/modules/client/src/main/java/org/apache/ignite/internal/client/table/AbstractClientView.java
index d665f5b096..9e0dfa5fca 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/client/table/AbstractClientView.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/client/table/AbstractClientView.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.client.table;
 
 import static java.util.stream.Collectors.toSet;
 import static org.apache.ignite.internal.client.ClientUtils.sync;
+import static org.apache.ignite.internal.table.criteria.CriteriaExceptionMapperUtil.mapToPublicCriteriaException;
 import static org.apache.ignite.internal.util.ExceptionUtils.unwrapCause;
 import static org.apache.ignite.lang.util.IgniteNameUtils.parseSimpleName;
 
@@ -143,11 +144,14 @@ abstract class AbstractClientView<T> implements CriteriaQuerySource<T> {
 
                                 return new QueryCriteriaAsyncCursor<>(resultSet, queryMapper(meta, schema), session::closeAsync);
                             })
-                            .exceptionally(th -> {
-                                session.closeAsync();
-
-                                throw new CompletionException(unwrapCause(th));
+                            .whenComplete((ignore, err) -> {
+                                if (err != null) {
+                                    session.closeAsync();
+                                }
                             });
+                })
+                .exceptionally(th -> {
+                    throw new CompletionException(mapToPublicCriteriaException(unwrapCause(th)));
                 });
     }
 }
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/lang/SqlExceptionMapperUtil.java b/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/CriteriaExceptionMapperUtil.java
similarity index 64%
copy from modules/sql-engine/src/main/java/org/apache/ignite/internal/lang/SqlExceptionMapperUtil.java
copy to modules/core/src/main/java/org/apache/ignite/internal/table/criteria/CriteriaExceptionMapperUtil.java
index a36119ccca..b9e1ec049f 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/lang/SqlExceptionMapperUtil.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/CriteriaExceptionMapperUtil.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.lang;
+package org.apache.ignite.internal.table.criteria;
 
 import static org.apache.ignite.internal.lang.IgniteExceptionMapperUtil.mapToPublicException;
 import static org.apache.ignite.lang.ErrorGroups.Common.INTERNAL_ERR;
@@ -23,42 +23,49 @@ import static org.apache.ignite.lang.ErrorGroups.Common.INTERNAL_ERR;
 import org.apache.ignite.lang.ErrorGroups.Common;
 import org.apache.ignite.lang.TraceableException;
 import org.apache.ignite.sql.SqlException;
+import org.apache.ignite.table.criteria.CriteriaException;
 
 /**
- * This utility class provides an ability to map Ignite internal exceptions to public SqlException.
+ * This utility class provides an ability to map internal SQL exceptions to public CriteriaException.
  */
-public class SqlExceptionMapperUtil {
-
+public class CriteriaExceptionMapperUtil {
     /**
-     * This method provides a mapping from internal exception to SQL public ones.
+     * This method provides a mapping from internal SQL exception to public ones.
      *
      * <p>The rules of mapping are the following:</p>
      * <ul>
      *     <li>any instance of {@link Error} is returned as is, except {@link AssertionError}
-     *     that will always be mapped to {@link SqlException} with the {@link Common#INTERNAL_ERR} error code.</li>
-     *     <li>any instance of {@link TraceableException} is wrapped into {@link SqlException}
+     *     that will always be mapped to {@link CriteriaException} with the {@link Common#INTERNAL_ERR} error code.</li>
+     *     <li>any instance of {@link SqlException} is wrapped into {@link CriteriaException} with the {@link Common#INTERNAL_ERR}
+     *         error code.</li>
+     *     <li>any instance of {@link TraceableException} is wrapped into {@link CriteriaException}
      *         with the original {@link TraceableException#traceId() traceUd} and {@link TraceableException#code() code}.</li>
      *     <li>if there are no any mappers that can do a mapping from the given error to a public exception,
-     *     then {@link SqlException} with the {@link Common#INTERNAL_ERR} error code is returned.</li>
+     *     then {@link CriteriaException} with the {@link Common#INTERNAL_ERR} error code is returned.</li>
      * </ul>
      *
      * @param origin Exception to be mapped.
      * @return Public exception.
      */
-    public static Throwable mapToPublicSqlException(Throwable origin) {
+    public static Throwable mapToPublicCriteriaException(Throwable origin) {
         Throwable e = mapToPublicException(origin);
+
         if (e instanceof Error) {
             return e;
         }
-        if (e instanceof SqlException) {
+        if (e instanceof CriteriaException) {
             return e;
         }
 
+        if (e instanceof SqlException) {
+            return new CriteriaException(INTERNAL_ERR, e);
+        }
+
         if (e instanceof TraceableException) {
             TraceableException traceable = (TraceableException) e;
-            return new SqlException(traceable.traceId(), traceable.code(), e.getMessage(), e);
+            return new CriteriaException(traceable.traceId(), traceable.code(), e.getMessage(), e);
         }
 
-        return new SqlException(INTERNAL_ERR, origin);
+        return new CriteriaException(INTERNAL_ERR, e);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/CursorAdapter.java b/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/CursorAdapter.java
index d1684943ba..c2acf02e85 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/CursorAdapter.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/CursorAdapter.java
@@ -17,14 +17,14 @@
 
 package org.apache.ignite.internal.table.criteria;
 
-import static org.apache.ignite.internal.lang.IgniteExceptionMapperUtil.convertToPublicFuture;
-import static org.apache.ignite.internal.lang.IgniteExceptionMapperUtil.mapToPublicException;
+import static org.apache.ignite.internal.util.ExceptionUtils.copyExceptionWithCause;
 import static org.apache.ignite.internal.util.ExceptionUtils.sneakyThrow;
 import static org.apache.ignite.internal.util.ExceptionUtils.unwrapCause;
 
 import java.util.Iterator;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
 import org.apache.ignite.lang.AsyncCursor;
 import org.apache.ignite.lang.Cursor;
 
@@ -54,9 +54,13 @@ public class CursorAdapter<T> implements Cursor<T> {
     @Override
     public void close() {
         try {
-            convertToPublicFuture(ac.closeAsync().toCompletableFuture()).join();
-        } catch (Throwable e) {
-            throw sneakyThrow(mapToPublicException(unwrapCause(e)));
+            ac.closeAsync().toCompletableFuture().get();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt(); // Restore interrupt flag.
+
+            throw sneakyThrow(unwrapCause(e));
+        } catch (ExecutionException e) {
+            throw sneakyThrow(copyExceptionWithCause(e));
         }
     }
 
@@ -92,8 +96,8 @@ public class CursorAdapter<T> implements Cursor<T> {
             } else if (nextPageStage != null) {
                 try {
                     curRes = nextPageStage.toCompletableFuture().join();
-                } catch (CompletionException ex) {
-                    throw sneakyThrow(unwrapCause(ex));
+                } catch (CompletionException e) {
+                    throw sneakyThrow(copyExceptionWithCause(e));
                 }
 
                 advance();
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/QueryCriteriaAsyncCursor.java b/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/QueryCriteriaAsyncCursor.java
index 1880faaa1a..8c03b5e731 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/QueryCriteriaAsyncCursor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/QueryCriteriaAsyncCursor.java
@@ -17,11 +17,14 @@
 
 package org.apache.ignite.internal.table.criteria;
 
+import static org.apache.ignite.internal.table.criteria.CriteriaExceptionMapperUtil.mapToPublicCriteriaException;
 import static org.apache.ignite.internal.util.CollectionUtils.mapIterable;
+import static org.apache.ignite.internal.util.ExceptionUtils.sneakyThrow;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.function.Function;
 import org.apache.ignite.lang.AsyncCursor;
+import org.apache.ignite.sql.NoRowSetExpectedException;
 import org.apache.ignite.sql.SqlRow;
 import org.apache.ignite.sql.async.AsyncResultSet;
 import org.jetbrains.annotations.Nullable;
@@ -57,13 +60,21 @@ public class QueryCriteriaAsyncCursor<T, R> implements AsyncCursor<T> {
     /** {@inheritDoc} */
     @Override
     public Iterable<T> currentPage() {
-        return mapIterable(ars.currentPage(), mapper, null);
+        try {
+            return mapIterable(ars.currentPage(), mapper, null);
+        } catch (NoRowSetExpectedException e) {
+            throw sneakyThrow(mapToPublicCriteriaException(e));
+        }
     }
 
     /** {@inheritDoc} */
     @Override
     public int currentPageSize() {
-        return ars.currentPageSize();
+        try {
+            return ars.currentPageSize();
+        } catch (NoRowSetExpectedException e) {
+            throw sneakyThrow(mapToPublicCriteriaException(e));
+        }
     }
 
     /** {@inheritDoc} */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/AsyncCursor.java b/modules/core/src/main/java/org/apache/ignite/internal/util/AsyncCursor.java
index b5277c0f06..1d0c6c5505 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/AsyncCursor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/AsyncCursor.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.util;
 
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.lang.CursorClosedException;
 
 /**
  * Asynchronous cursor.
@@ -31,11 +32,11 @@ public interface AsyncCursor<T> {
      *
      * <p>Several calls to this method should be chained and resulting stages should be completed in the order of invocation. Any call
      * to this method after call to {@link #closeAsync()} should be completed immediately with
-     * {@link org.apache.ignite.sql.CursorClosedException} even if the future returned by {@link #closeAsync()}
-     * is not completed yet.
+     * {@link CursorClosedException} even if the future returned by {@link #closeAsync()} is not completed yet.
      *
      * @param rows Desired amount of rows.
      * @return A completion stage that will be completed with batch of size {@code rows} or less if there is no more data.
+     * @throws CursorClosedException If cursor is closed.
      */
     CompletableFuture<BatchedResult<T>> requestNextAsync(int rows);
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/AsyncWrapper.java b/modules/core/src/main/java/org/apache/ignite/internal/util/AsyncWrapper.java
index adcdd54b3d..f14fc9b43f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/AsyncWrapper.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/AsyncWrapper.java
@@ -26,7 +26,7 @@ import java.util.NoSuchElementException;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
 import java.util.function.Function;
-import org.apache.ignite.sql.CursorClosedException;
+import org.apache.ignite.lang.CursorClosedException;
 
 /**
  * Wrapper that converts a synchronous iterator to an asynchronous one.
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/table/criteria/CriteriaExceptionMapperUtilTest.java b/modules/core/src/test/java/org/apache/ignite/internal/table/criteria/CriteriaExceptionMapperUtilTest.java
new file mode 100644
index 0000000000..49a5dead8a
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/table/criteria/CriteriaExceptionMapperUtilTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.internal.table.criteria;
+
+import static org.apache.ignite.internal.table.criteria.CriteriaExceptionMapperUtil.mapToPublicCriteriaException;
+import static org.apache.ignite.lang.ErrorGroups.Common.INTERNAL_ERR;
+import static org.apache.ignite.lang.ErrorGroups.Sql.EXECUTION_CANCELLED_ERR;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.apache.ignite.internal.lang.IgniteInternalException;
+import org.apache.ignite.sql.NoRowSetExpectedException;
+import org.apache.ignite.table.criteria.CriteriaException;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests mapping internal exceptions to public {@link CriteriaException}.
+ */
+class CriteriaExceptionMapperUtilTest {
+    /**
+     * Tests a default mapping of internal exceptions passed from the sql engine.
+     */
+    @Test
+    public void testMappingForInternalException() {
+        IgniteInternalException internalErr = new IgniteInternalException(EXECUTION_CANCELLED_ERR);
+        Throwable mappedErr = mapToPublicCriteriaException(internalErr);
+
+        assertThat(mappedErr, instanceOf(CriteriaException.class));
+
+        CriteriaException mappedCriteriaErr = (CriteriaException) mappedErr;
+
+        assertThat("Mapped exception should have the same trace identifier.", mappedCriteriaErr.traceId(), is(internalErr.traceId()));
+        assertThat("Mapped exception shouldn't have the same error code.", mappedCriteriaErr.code(), is(INTERNAL_ERR));
+    }
+
+    /**
+     * Tests a default mapping of exceptions passed from the sql engine.
+     */
+    @Test
+    public void testMappingForSqlException() {
+        NoRowSetExpectedException sqlErr = new NoRowSetExpectedException();
+        Throwable mappedErr = mapToPublicCriteriaException(sqlErr);
+
+        assertThat(mappedErr, instanceOf(CriteriaException.class));
+
+        CriteriaException mappedCriteriaErr = (CriteriaException) mappedErr;
+
+        assertThat("Mapped exception should have the same trace identifier.", mappedCriteriaErr.traceId(), is(sqlErr.traceId()));
+        assertThat("Mapped exception shouldn't have the same error code.", mappedCriteriaErr.code(), is(INTERNAL_ERR));
+    }
+
+    /**
+     * Tests a default mapping of exceptions.
+     */
+    @Test
+    public void testMappingForCriteriaException() {
+        CriteriaException criteriaErr = new CriteriaException(INTERNAL_ERR, new NoRowSetExpectedException());
+
+        Throwable mappedErr = mapToPublicCriteriaException(criteriaErr);
+
+        assertSame(criteriaErr, mappedErr);
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/AsyncWrapperSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/AsyncWrapperSelfTest.java
index 6b88e84d93..3751211f7b 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/util/AsyncWrapperSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/util/AsyncWrapperSelfTest.java
@@ -33,7 +33,7 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.ForkJoinPool;
 import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
-import org.apache.ignite.sql.SqlException;
+import org.apache.ignite.lang.CursorClosedException;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
@@ -159,14 +159,14 @@ public class AsyncWrapperSelfTest extends BaseIgniteAbstractTest {
                 .thenAccept(batch -> assertThat(batch.items(), equalTo(data.subList(0, 1))))
                 .exceptionally(ex -> {
                     assertInstanceOf(CompletionException.class, ex);
-                    assertInstanceOf(SqlException.class, ex.getCause());
+                    assertInstanceOf(CursorClosedException.class, ex.getCause());
 
                     return null;
                 });
         var stage2 = cursor.closeAsync();
         var stage3 = cursor.requestNextAsync(1)
                 .exceptionally(ex -> {
-                    assertInstanceOf(SqlException.class, ex);
+                    assertInstanceOf(CursorClosedException.class, ex);
 
                     return null;
                 });
diff --git a/modules/platforms/cpp/ignite/common/error_codes.h b/modules/platforms/cpp/ignite/common/error_codes.h
index 03bdff6fe0..20ba88a4f2 100644
--- a/modules/platforms/cpp/ignite/common/error_codes.h
+++ b/modules/platforms/cpp/ignite/common/error_codes.h
@@ -63,6 +63,7 @@ enum class code : underlying_t {
     ILLEGAL_ARGUMENT = 0x10003,
     SSL_CONFIGURATION = 0x10004,
     NODE_LEFT = 0x10005,
+    CURSOR_CLOSED = 0x10006,
     INTERNAL = 0x1ffff,
 
     // Table group. Group code: 2
@@ -87,10 +88,8 @@ enum class code : underlying_t {
     HANDSHAKE_HEADER = 0x3000a,
 
     // Sql group. Group code: 4
-    CURSOR_NO_MORE_PAGES = 0x40001,
     QUERY_NO_RESULT_SET = 0x40002,
     SCHEMA_NOT_FOUND = 0x40003,
-    CURSOR_CLOSED = 0x40004,
     STMT_PARSE = 0x40005,
     STMT_VALIDATION = 0x40006,
     CONSTRAINT_VIOLATION = 0x40007,
diff --git a/modules/platforms/cpp/ignite/odbc/common_types.cpp b/modules/platforms/cpp/ignite/odbc/common_types.cpp
index 7883b0648f..9a3bde68c8 100644
--- a/modules/platforms/cpp/ignite/odbc/common_types.cpp
+++ b/modules/platforms/cpp/ignite/odbc/common_types.cpp
@@ -112,6 +112,8 @@ environment_attribute environment_attribute_to_internal(int32_t attr) {
 sql_state error_code_to_sql_state(error::code code) {
     switch (code) {
         // Common group. Group code: 1
+        case error::code::CURSOR_CLOSED:
+            return sql_state::S24000_INVALID_CURSOR_STATE;
         case error::code::NODE_STOPPING:
         case error::code::COMPONENT_NOT_STARTED:
         case error::code::ILLEGAL_ARGUMENT:
@@ -150,10 +152,6 @@ sql_state error_code_to_sql_state(error::code code) {
             return sql_state::S08004_CONNECTION_REJECTED;
 
         // Sql group. Group code: 4
-        case error::code::CURSOR_CLOSED:
-            return sql_state::S24000_INVALID_CURSOR_STATE;
-        case error::code::CURSOR_NO_MORE_PAGES:
-            return sql_state::S24000_INVALID_CURSOR_STATE;
         case error::code::SCHEMA_NOT_FOUND:
             return sql_state::S3F000_INVALID_SCHEMA_NAME;
         case error::code::PLANNING_TIMEOUT:
diff --git a/modules/platforms/dotnet/Apache.Ignite/ErrorCodes.g.cs b/modules/platforms/dotnet/Apache.Ignite/ErrorCodes.g.cs
index 75cdff906c..36e5ad094f 100644
--- a/modules/platforms/dotnet/Apache.Ignite/ErrorCodes.g.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/ErrorCodes.g.cs
@@ -75,6 +75,9 @@ namespace Apache.Ignite
             /// <summary> NodeLeft error. </summary>
             public const int NodeLeft = (GroupCode << 16) | (5 & 0xFFFF);
 
+            /// <summary> CursorClosed error. </summary>
+            public const int CursorClosed = (GroupCode << 16) | (6 & 0xFFFF);
+
             /// <summary> Internal error. </summary>
             public const int Internal = (GroupCode << 16) | (65535 & 0xFFFF);
         }
@@ -159,18 +162,12 @@ namespace Apache.Ignite
             /// <summary> Sql group name. </summary>
             public const String GroupName = "SQL";
 
-            /// <summary> CursorNoMorePages error. </summary>
-            public const int CursorNoMorePages = (GroupCode << 16) | (1 & 0xFFFF);
-
             /// <summary> QueryNoResultSet error. </summary>
             public const int QueryNoResultSet = (GroupCode << 16) | (2 & 0xFFFF);
 
             /// <summary> SchemaNotFound error. </summary>
             public const int SchemaNotFound = (GroupCode << 16) | (3 & 0xFFFF);
 
-            /// <summary> CursorClosed error. </summary>
-            public const int CursorClosed = (GroupCode << 16) | (4 & 0xFFFF);
-
             /// <summary> StmtParse error. </summary>
             public const int StmtParse = (GroupCode << 16) | (5 & 0xFFFF);
 
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ResultSet.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ResultSet.cs
index a2bd3a6f52..170c1a26c5 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ResultSet.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ResultSet.cs
@@ -403,7 +403,7 @@ namespace Apache.Ignite.Internal.Sql
 
             if (_iterated)
             {
-                throw new IgniteClientException(ErrorGroups.Sql.CursorClosed, "Query result set can not be iterated more than once.");
+                throw new IgniteClientException(ErrorGroups.Common.CursorClosed, "Query result set can not be iterated more than once.");
             }
 
             _iterated = true;
diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItCommonApiTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItCommonApiTest.java
index a705c6ea4e..08ded62911 100644
--- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItCommonApiTest.java
+++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItCommonApiTest.java
@@ -32,8 +32,8 @@ import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.internal.sql.BaseSqlIntegrationTest;
+import org.apache.ignite.lang.CursorClosedException;
 import org.apache.ignite.lang.IgniteException;
-import org.apache.ignite.sql.CursorClosedException;
 import org.apache.ignite.sql.IgniteSql;
 import org.apache.ignite.sql.ResultSet;
 import org.apache.ignite.sql.Session;
diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlApiBaseTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlApiBaseTest.java
index d656ebe3dc..0616fadd41 100644
--- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlApiBaseTest.java
+++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlApiBaseTest.java
@@ -43,13 +43,14 @@ import org.apache.ignite.internal.sql.BaseSqlIntegrationTest;
 import org.apache.ignite.internal.sql.api.ColumnMetadataImpl.ColumnOriginImpl;
 import org.apache.ignite.internal.testframework.IgniteTestUtils;
 import org.apache.ignite.internal.tx.TxManager;
+import org.apache.ignite.lang.CursorClosedException;
+import org.apache.ignite.lang.ErrorGroups.Common;
 import org.apache.ignite.lang.ErrorGroups.Sql;
 import org.apache.ignite.lang.ErrorGroups.Transactions;
 import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.sql.BatchedArguments;
 import org.apache.ignite.sql.ColumnMetadata;
 import org.apache.ignite.sql.ColumnType;
-import org.apache.ignite.sql.CursorClosedException;
 import org.apache.ignite.sql.IgniteSql;
 import org.apache.ignite.sql.NoRowSetExpectedException;
 import org.apache.ignite.sql.ResultSet;
@@ -480,12 +481,12 @@ public abstract class ItSqlApiBaseTest extends BaseSqlIntegrationTest {
 
         ses.close();
 
-        SqlException sqlEx = assertThrowsSqlException(
-                Sql.CURSOR_CLOSED_ERR,
-                "Cursor is closed",
-                () -> rs.forEachRemaining(System.out::println));
+        IgniteTestUtils.assertThrowsWithCode(
+                CursorClosedException.class,
+                Common.CURSOR_CLOSED_ERR,
+                () -> rs.forEachRemaining(System.out::println),
+                "Cursor is closed");
 
-        assertTrue(IgniteTestUtils.hasCause(sqlEx, CursorClosedException.class, null));
         assertThrowsSqlException(Sql.SESSION_CLOSED_ERR, "Session is closed", () -> execute(ses, "SELECT ID FROM TEST"));
     }
 
@@ -538,11 +539,11 @@ public abstract class ItSqlApiBaseTest extends BaseSqlIntegrationTest {
             Thread.sleep(300); // ResultSetImpl fetches next page in background, wait to it to complete to avoid flakiness.
             rs.close();
 
-            assertThrowsSqlException(
+            IgniteTestUtils.assertThrowsWithCode(
                     CursorClosedException.class,
-                    Sql.CURSOR_CLOSED_ERR,
-                    "Cursor is closed",
-                    () -> rs.forEachRemaining(Object::hashCode));
+                    Common.CURSOR_CLOSED_ERR,
+                    () -> rs.forEachRemaining(Object::hashCode),
+                    "Cursor is closed");
         }
     }
 
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/lang/SqlExceptionMapperUtil.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/lang/SqlExceptionMapperUtil.java
index a36119ccca..33be23771a 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/lang/SqlExceptionMapperUtil.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/lang/SqlExceptionMapperUtil.java
@@ -20,6 +20,7 @@ package org.apache.ignite.internal.lang;
 import static org.apache.ignite.internal.lang.IgniteExceptionMapperUtil.mapToPublicException;
 import static org.apache.ignite.lang.ErrorGroups.Common.INTERNAL_ERR;
 
+import org.apache.ignite.lang.CursorClosedException;
 import org.apache.ignite.lang.ErrorGroups.Common;
 import org.apache.ignite.lang.TraceableException;
 import org.apache.ignite.sql.SqlException;
@@ -36,6 +37,7 @@ public class SqlExceptionMapperUtil {
      * <ul>
      *     <li>any instance of {@link Error} is returned as is, except {@link AssertionError}
      *     that will always be mapped to {@link SqlException} with the {@link Common#INTERNAL_ERR} error code.</li>
+     *     <li>any instance of {@link SqlException}, {@link CursorClosedException} is returned as is</li>
      *     <li>any instance of {@link TraceableException} is wrapped into {@link SqlException}
      *         with the original {@link TraceableException#traceId() traceUd} and {@link TraceableException#code() code}.</li>
      *     <li>if there are no any mappers that can do a mapping from the given error to a public exception,
@@ -50,7 +52,7 @@ public class SqlExceptionMapperUtil {
         if (e instanceof Error) {
             return e;
         }
-        if (e instanceof SqlException) {
+        if (e instanceof SqlException || e instanceof CursorClosedException) {
             return e;
         }
 
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/AsyncResultSetImpl.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/AsyncResultSetImpl.java
index 4bd0ea638f..13e2422cb9 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/AsyncResultSetImpl.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/AsyncResultSetImpl.java
@@ -17,8 +17,6 @@
 
 package org.apache.ignite.internal.sql.api;
 
-import static org.apache.ignite.lang.ErrorGroups.Sql.CURSOR_NO_MORE_PAGES_ERR;
-
 import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -34,7 +32,6 @@ import org.apache.ignite.internal.util.AsyncCursor.BatchedResult;
 import org.apache.ignite.internal.util.TransformingIterator;
 import org.apache.ignite.sql.NoRowSetExpectedException;
 import org.apache.ignite.sql.ResultSetMetadata;
-import org.apache.ignite.sql.SqlException;
 import org.apache.ignite.sql.SqlRow;
 import org.apache.ignite.sql.async.AsyncResultSet;
 import org.apache.ignite.table.Tuple;
@@ -44,9 +41,6 @@ import org.jetbrains.annotations.Nullable;
  * Asynchronous result set implementation.
  */
 public class AsyncResultSetImpl<T> implements AsyncResultSet<T> {
-    private static final CompletableFuture<? extends AsyncResultSet<?>> HAS_NO_MORE_PAGE_FUTURE =
-            CompletableFuture.failedFuture(new SqlException(CURSOR_NO_MORE_PAGES_ERR, "There are no more pages."));
-
     private final IdleExpirationTracker expirationTracker;
 
     private final AsyncSqlCursor<InternalSqlRow> cursor;
@@ -141,20 +135,16 @@ public class AsyncResultSetImpl<T> implements AsyncResultSet<T> {
 
         expirationTracker.touch();
 
-        if (!hasMorePages()) {
-            return (CompletableFuture<? extends AsyncResultSet<T>>) HAS_NO_MORE_PAGE_FUTURE;
-        } else {
-            return cursor.requestNextAsync(pageSize)
-                    .thenApply(page -> {
-                        curPage = page;
+        return cursor.requestNextAsync(pageSize)
+                .thenApply(page -> {
+                    curPage = page;
 
-                        if (!curPage.hasMore()) {
-                            closeAsync();
-                        }
+                    if (!curPage.hasMore()) {
+                        closeAsync();
+                    }
 
-                        return this;
-                    });
-        }
+                    return this;
+                });
     }
 
     /** {@inheritDoc} */
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/AsyncRootNode.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/AsyncRootNode.java
index 0f8b2f5408..e0026a757d 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/AsyncRootNode.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/AsyncRootNode.java
@@ -30,7 +30,7 @@ import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
 import org.apache.ignite.internal.sql.engine.QueryCancelledException;
 import org.apache.ignite.internal.util.AsyncCursor;
-import org.apache.ignite.sql.CursorClosedException;
+import org.apache.ignite.lang.CursorClosedException;
 import org.jetbrains.annotations.Nullable;
 
 /**
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/lang/SqlExceptionMapperUtilTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/lang/SqlExceptionMapperUtilTest.java
index 807909ecdc..4b05df7263 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/lang/SqlExceptionMapperUtilTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/lang/SqlExceptionMapperUtilTest.java
@@ -25,9 +25,14 @@ import static org.hamcrest.Matchers.instanceOf;
 import static org.hamcrest.Matchers.is;
 import static org.junit.jupiter.api.Assertions.assertSame;
 
+import java.util.stream.Stream;
+import org.apache.ignite.lang.CursorClosedException;
 import org.apache.ignite.sql.NoRowSetExpectedException;
 import org.apache.ignite.sql.SqlException;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 /**
  * Tests mapping internal exceptions to public SqlException.
@@ -49,16 +54,22 @@ class SqlExceptionMapperUtilTest {
         assertThat("Mapped exception shouldn't have the same error code.", mappedSqlErr.code(), is(INTERNAL_ERR));
     }
 
+    private static Stream<Arguments> testSqlInternalExceptionDefaultMappingForPublicException() {
+        return Stream.of(
+                Arguments.of(new NoRowSetExpectedException()),
+                Arguments.of(new CursorClosedException())
+        );
+    }
+
     /**
      * Tests a default mapping of internal exceptions passed from the sql engine.
      */
-    @Test
-    public void testSqlInternalExceptionDefaultMappingForSqlException() {
-        NoRowSetExpectedException sqlErr = new NoRowSetExpectedException();
-
-        Throwable mappedErr = mapToPublicSqlException(sqlErr);
+    @ParameterizedTest
+    @MethodSource
+    public void testSqlInternalExceptionDefaultMappingForPublicException(Throwable err) {
+        Throwable mappedErr = mapToPublicSqlException(err);
 
-        assertSame(sqlErr, mappedErr);
+        assertSame(err, mappedErr);
     }
 
     /**
diff --git a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItCriteriaQueryTest.java b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItCriteriaQueryTest.java
index 754682b0aa..b136f6f706 100644
--- a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItCriteriaQueryTest.java
+++ b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItCriteriaQueryTest.java
@@ -23,6 +23,7 @@ import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toMap;
 import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
 import static org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrows;
+import static org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrowsWithCode;
 import static org.apache.ignite.internal.testframework.IgniteTestUtils.await;
 import static org.apache.ignite.internal.testframework.matchers.TupleMatcher.tupleValue;
 import static org.apache.ignite.lang.util.IgniteNameUtils.quote;
@@ -63,10 +64,12 @@ import org.apache.ignite.internal.sql.api.IgniteSqlImpl;
 import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.lang.AsyncCursor;
 import org.apache.ignite.lang.Cursor;
-import org.apache.ignite.lang.IgniteException;
+import org.apache.ignite.lang.CursorClosedException;
+import org.apache.ignite.lang.ErrorGroups.Common;
 import org.apache.ignite.table.RecordView;
 import org.apache.ignite.table.Table;
 import org.apache.ignite.table.Tuple;
+import org.apache.ignite.table.criteria.CriteriaException;
 import org.apache.ignite.table.criteria.CriteriaQuerySource;
 import org.apache.ignite.table.mapper.Mapper;
 import org.apache.ignite.tx.Transaction;
@@ -152,7 +155,7 @@ public class ItCriteriaQueryTest extends ClusterPerClassIntegrationTest {
     @MethodSource
     public <T> void testRecordViewQuery(CriteriaQuerySource<T> view, Function<T, Tuple> mapper) {
         assertThrows(
-                IgniteException.class,
+                CriteriaException.class,
                 () -> view.query(null, columnValue("id", equalTo("2"))),
                 "Dynamic parameter requires adding explicit type cast"
         );
@@ -257,7 +260,7 @@ public class ItCriteriaQueryTest extends ClusterPerClassIntegrationTest {
     @MethodSource
     public <T> void testKeyValueView(CriteriaQuerySource<T> view, Function<T, Entry<Tuple, Tuple>> mapper) {
         assertThrows(
-                IgniteException.class,
+                CriteriaException.class,
                 () -> view.query(null, columnValue("id", equalTo("2"))),
                 "Dynamic parameter requires adding explicit type cast"
         );
@@ -413,6 +416,40 @@ public class ItCriteriaQueryTest extends ClusterPerClassIntegrationTest {
         await(ars.closeAsync());
     }
 
+    private static Stream<Arguments> allViews() {
+        Table table = CLUSTER.aliveNode().tables().table(TABLE_NAME);
+        Table clientTable = CLIENT.tables().table(TABLE_NAME);
+
+        return Stream.of(
+                Arguments.of(table.keyValueView()),
+                Arguments.of(table.keyValueView(TestObjectKey.class, TestObject.class)),
+                Arguments.of(clientTable.keyValueView()),
+                Arguments.of(clientTable.keyValueView(TestObjectKey.class, TestObject.class))
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("allViews")
+    void testFetchCursorIsClosed(CriteriaQuerySource<TestObject> view) {
+        AsyncCursor<TestObject> ars1 = await(view.queryAsync(null, null, builder().pageSize(2).build()));
+
+        assertNotNull(ars1);
+        await(ars1.closeAsync());
+        assertThrowsWithCode(CursorClosedException.class, Common.CURSOR_CLOSED_ERR, () -> await(ars1.fetchNextPage()), "Cursor is closed");
+
+        AsyncCursor<TestObject> ars2 = await(view.queryAsync(null, null, builder().pageSize(3).build()));
+
+        assertNotNull(ars2);
+        assertThrowsWithCode(CursorClosedException.class, Common.CURSOR_CLOSED_ERR, () -> await(ars2.fetchNextPage()), "Cursor is closed");
+    }
+
+    @ParameterizedTest
+    @MethodSource("allViews")
+    <T> void testInvalidColumnName(CriteriaQuerySource<T> view) {
+        assertThrows(CriteriaException.class, () -> await(view.queryAsync(null, columnValue("id1", equalTo(2)))),
+                "Unexpected column name: ID1");
+    }
+
     private static Stream<Arguments> testRecordViewWithQuotes() {
         Table table = CLUSTER.aliveNode().tables().table(QUOTED_TABLE_NAME);
         Table clientTable = CLIENT.tables().table(QUOTED_TABLE_NAME);
@@ -494,7 +531,7 @@ public class ItCriteriaQueryTest extends ClusterPerClassIntegrationTest {
         int baseSessionsCount = activeSessionsCount();
 
         assertThrows(
-                IgniteException.class,
+                CriteriaException.class,
                 () -> view.query(tx, columnValue("id", equalTo(2))),
                 "Transaction is already finished"
         );
diff --git a/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractTableView.java b/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractTableView.java
index 66cb9dbe89..20980b1df2 100644
--- a/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractTableView.java
+++ b/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractTableView.java
@@ -20,6 +20,7 @@ package org.apache.ignite.internal.table;
 import static java.util.concurrent.CompletableFuture.completedFuture;
 import static java.util.function.Function.identity;
 import static org.apache.ignite.internal.lang.IgniteExceptionMapperUtil.convertToPublicFuture;
+import static org.apache.ignite.internal.table.criteria.CriteriaExceptionMapperUtil.mapToPublicCriteriaException;
 import static org.apache.ignite.internal.util.ExceptionUtils.isOrCausedBy;
 import static org.apache.ignite.internal.util.ExceptionUtils.sneakyThrow;
 import static org.apache.ignite.internal.util.ExceptionUtils.unwrapCause;
@@ -213,12 +214,15 @@ abstract class AbstractTableView<R> implements CriteriaQuerySource<R> {
 
                         return new QueryCriteriaAsyncCursor<>(resultSet, queryMapper(meta, schema), session::closeAsync);
                     })
-                    .exceptionally(th -> {
-                        session.closeAsync();
-
-                        throw new CompletionException(unwrapCause(th));
+                    .whenComplete((ignore, err) -> {
+                        if (err != null) {
+                            session.closeAsync();
+                        }
                     });
-        });
+        })
+                .exceptionally(th -> {
+                    throw new CompletionException(mapToPublicCriteriaException(unwrapCause(th)));
+                });
     }