You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by aa...@apache.org on 2026/09/12 18:30:42 UTC

(cayenne) 02/02: CAY-3024 Replace QueryResponse with List

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

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit 4b3133f90eb5ab7146bfa7f196bc96a0f4ff6207
Author: Andrus Adamchik <an...@objectstyle.com>
AuthorDate: Sat Sep 12 14:11:52 2026 -0400

    CAY-3024 Replace QueryResponse with List<QueryResultItem>
---
 RELEASE-NOTES.txt                                  |   1 +
 UPGRADE.md                                         |  14 +-
 .../apache/cayenne/lifecycle/id/StringIdQuery.java |   4 +-
 .../relationship/ObjectIdRelationshipFilter.java   |   8 +-
 .../cayenne/lifecycle/id/StringIdQueryTest.java    |  17 +-
 .../main/java/org/apache/cayenne/DataChannel.java  |  11 +-
 .../org/apache/cayenne/DataChannelQueryFilter.java |  14 +-
 .../cayenne/DataChannelQueryFilterChain.java       |   4 +-
 .../java/org/apache/cayenne/ObjectContext.java     |   6 +-
 .../java/org/apache/cayenne/QueryResponse.java     | 165 -----------------
 .../org/apache/cayenne/access/DataContext.java     |  21 +--
 .../apache/cayenne/access/DataContextChannel.java  |  25 ++-
 .../cayenne/access/DataContextQueryAction.java     |  18 +-
 .../java/org/apache/cayenne/access/DataDomain.java |  10 +-
 .../cayenne/access/DataDomainIdQueryAction.java    |   2 +-
 .../cayenne/access/DataDomainQueryAction.java      |  88 ++++-----
 .../access/DataDomainRelationshipAction.java       |   2 +-
 .../apache/cayenne/access/ListWithPrefetches.java  |   2 +-
 .../org/apache/cayenne/access/ObjectStore.java     |   2 +-
 .../apache/cayenne/access/QueryResultItems.java    |  71 +++++++
 .../java/org/apache/cayenne/query/MappedExec.java  |  15 +-
 .../org/apache/cayenne/query/ProcedureCall.java    |   2 +-
 .../org/apache/cayenne/query/ProcedureQuery.java   |   3 +-
 .../org/apache/cayenne/query/QueryResultItems.java |  52 ------
 .../java/org/apache/cayenne/query/SQLExec.java     |   5 +-
 .../org/apache/cayenne/util/GenericResponse.java   | 205 ---------------------
 .../java/org/apache/cayenne/util/ListResponse.java |  90 ---------
 .../java/org/apache/cayenne/MockDataChannel.java   |  31 ++--
 .../access/DataContextDecoratedStackIT.java        |   4 +-
 .../cayenne/access/DataContextEJBQLDeleteIT.java   |  15 +-
 .../cayenne/access/DataContextEJBQLDeletePKIT.java |   6 +-
 .../DataContextEJBQLNumericalFunctionalIT.java     |   6 +-
 .../access/DataContextEJBQLUpdateCompoundIT.java   |   7 +-
 .../cayenne/access/DataContextEJBQLUpdateIT.java   |  31 ++--
 .../access/DataContextProcedureQueryIT.java        |  12 +-
 .../apache/cayenne/access/DataDomainFiltersIT.java |  19 +-
 .../runtime/DataDomainProviderIT.java              |   7 +-
 .../org/apache/cayenne/query/MappedQueryIT.java    |   9 +-
 .../apache/cayenne/util/GenericResponseTest.java   |  72 --------
 .../org/apache/cayenne/util/ListResponseTest.java  |  67 -------
 .../_cayenne-guide/part2/objectContext.adoc        |  10 +-
 41 files changed, 305 insertions(+), 848 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index c6760dc79..5d1a2ff67 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -29,6 +29,7 @@ CAY-3019 Skip noop conditions in generated SQL
 CAY-3020 Remove serialization contracts
 CAY-3021 Get rid of ObjectIdQuery
 CAY-3023 Replace QueryResult with a List of redesigned QueryResultItems
+CAY-3024 Replace QueryResponse with List<QueryResultItem>
 
 Bug Fixes:
 
diff --git a/UPGRADE.md b/UPGRADE.md
index 38fce38de..2cb0f6650 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -187,12 +187,16 @@ Expression caseWhenExp = caseWhen(
   ```
 
    Stored procedure OUT parameters are no longer disguised as a one-row result set. They arrive as a dedicated
-   `QueryResultItem.OutParameters` item (a map keyed by parameter name), `QueryResponse` reports them via
-   `isOutParameters()` / `currentOutParameters()`.
+   `QueryResultItem.OutParameters` item (a map keyed by parameter name).
 
    The cgen templates now emit `List<QueryResultItem>` instead of `QueryResult<?>` for the `perform*` methods of
    mapped exec queries, so regenerate your classes via Modeler ("Tools" → "Generate Classes") or the AI plugin
-   if you have generated classes with multi-part queries.
+   if you have generated classes with multipart queries.
+
+*  Per [CAY-3024](https://issues.apache.org/jira/browse/CAY-3024) `org.apache.cayenne.QueryResponse` was removed. Everything that
+   used to return a `QueryResponse` - `ObjectContext.performGenericQuery(..)`, etc. - now return
+   `List<QueryResultItem>`. See CAY-3023 above for the example of how to process the result. Custom 
+   `DataChannelQueryFilter` implementations now require a new signature.
 
 *  The `org.apache.cayenne.query.ParameterizedQuery` interface was removed, together with the `createQuery(Map)`
   methods of `SQLTemplate`, `ProcedureQuery` and `ObjectSelect` that implemented it. Applying parameters to a mapped
@@ -218,8 +222,8 @@ Expression caseWhenExp = caseWhen(
   query filters need to add the parameter and pass it down the chain:
 
   ```java
-  public QueryResponse onQuery(ObjectContext context, Query query, boolean iteratedResult,
-                               DataChannelQueryFilterChain chain) {
+  public List<QueryResultItem> onQuery(ObjectContext context, Query query, boolean iteratedResult,
+                                       DataChannelQueryFilterChain chain) {
       return chain.onQuery(context, query, iteratedResult);
   }
   ```
diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/id/StringIdQuery.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/id/StringIdQuery.java
index 1c7196634..9ed861534 100644
--- a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/id/StringIdQuery.java
+++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/id/StringIdQuery.java
@@ -48,8 +48,8 @@ import java.util.Map;
  * A query that allows to fetch objects based on one or more String IDs. The returned
  * objects do not have to be of the same type, be related via inheritance, or come from
  * the same DB table. Note that if you expect multiple types of objects, use
- * {@link ObjectContext#performGenericQuery(Query)}. The returned QueryResponse will
- * contain separate lists of DataRows for each type in no particular order.
+ * {@link ObjectContext#performGenericQuery(Query)}. The returned result will contain a separate
+ * {@link org.apache.cayenne.QueryResultItem.Select} of DataRows for each type in no particular order.
  * <p>
  * As of this writing, a limitation of this query is that it returns DataRows that need to
  * be manually converted to objects if needed.
diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/relationship/ObjectIdRelationshipFilter.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/relationship/ObjectIdRelationshipFilter.java
index dfa4ac1fb..700550275 100644
--- a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/relationship/ObjectIdRelationshipFilter.java
+++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/relationship/ObjectIdRelationshipFilter.java
@@ -22,12 +22,14 @@ import org.apache.cayenne.DataChannelQueryFilter;
 import org.apache.cayenne.DataChannelQueryFilterChain;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.Persistent;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.annotation.PostLoad;
 import org.apache.cayenne.annotation.PostPersist;
 import org.apache.cayenne.annotation.PostUpdate;
 import org.apache.cayenne.query.Query;
 
+import java.util.List;
+
 /**
  * A {@link DataChannelQueryFilter} that implements ObjectId relationships read functionality.
  * 
@@ -45,8 +47,8 @@ public class ObjectIdRelationshipFilter implements DataChannelQueryFilter {
         return new ObjectIdRelationshipBatchFaultingStrategy();
     }
 
-    public QueryResponse onQuery(ObjectContext context, Query query, boolean iteratedResult,
-                                 DataChannelQueryFilterChain chain) {
+    public List<QueryResultItem> onQuery(ObjectContext context, Query query, boolean iteratedResult,
+                                         DataChannelQueryFilterChain chain) {
         try {
             return chain.onQuery(context, query, iteratedResult);
         } finally {
diff --git a/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/id/StringIdQueryTest.java b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/id/StringIdQueryTest.java
index 3e9bc1561..865a5179f 100644
--- a/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/id/StringIdQueryTest.java
+++ b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/id/StringIdQueryTest.java
@@ -19,7 +19,7 @@
 package org.apache.cayenne.lifecycle.id;
 
 import org.apache.cayenne.DataRow;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.configuration.DataNodeDescriptor;
 import org.apache.cayenne.datasource.CayenneDataSource;
 import org.apache.cayenne.runtime.CayenneRuntime;
@@ -96,16 +96,17 @@ public class StringIdQueryTest {
         e1Helper.insert(3).insert(4);
 
         StringIdQuery query = new StringIdQuery("E1:3", "E1:4", "E1:5");
-        QueryResponse response = runtime.newContext().performGenericQuery(query);
+        List<QueryResultItem> response = runtime.newContext().performGenericQuery(query);
         assertEquals(1, response.size());
-        assertEquals(2, response.firstList().size());
+        List<?> rows = ((QueryResultItem.Select<?>) response.getFirst()).objects();
+        assertEquals(2, rows.size());
 
         Set<Number> ids = new HashSet<>();
 
-        DataRow r1 = (DataRow) response.firstList().get(0);
+        DataRow r1 = (DataRow) rows.get(0);
         ids.add((Number) r1.get("ID"));
 
-        DataRow r2 = (DataRow) response.firstList().get(1);
+        DataRow r2 = (DataRow) rows.get(1);
         ids.add((Number) r2.get("ID"));
 
         assertTrue(ids.contains(3L));
@@ -121,14 +122,14 @@ public class StringIdQueryTest {
         e2Helper.insert(5).insert(6).insert(7);
 
         StringIdQuery query = new StringIdQuery("E1:3", "E1:4", "E2:6", "E1:5");
-        QueryResponse response = runtime.newContext().performGenericQuery(query);
+        List<QueryResultItem> response = runtime.newContext().performGenericQuery(query);
         assertEquals(2, response.size());
 
         Set<String> ids = new HashSet<>();
 
-        while (response.next()) {
+        for (QueryResultItem item : response) {
             @SuppressWarnings("unchecked")
-            List<DataRow> list = (List<DataRow>) response.currentList();
+            List<DataRow> list = ((QueryResultItem.Select<DataRow>) item).objects();
             for (DataRow row : list) {
                 ids.add(row.getEntityName() + ":" + row.get("ID"));
             }
diff --git a/cayenne/src/main/java/org/apache/cayenne/DataChannel.java b/cayenne/src/main/java/org/apache/cayenne/DataChannel.java
index d76e5f347..6d0e347be 100644
--- a/cayenne/src/main/java/org/apache/cayenne/DataChannel.java
+++ b/cayenne/src/main/java/org/apache/cayenne/DataChannel.java
@@ -113,13 +113,14 @@ public interface DataChannel {
      *
      * @param context        an ObjectContext that originated the query, used to register result objects.
      * @param query          a query to execute.
-     * @param iteratedResult if true, the result is returned as a {@link ResultIterator} accessible via
-     *                       {@link QueryResponse#firstIterator()}, and the caller is responsible for closing
-     *                       it. If false, the result is fully read into a list.
-     * @return a generic response object that encapsulates result of the execution.
+     * @param iteratedResult if true, the result is returned as a {@link ResultIterator} wrapped in a
+     *                       {@link QueryResultItem.Iterator}, and the caller is responsible for closing it. If false,
+     *                       the result is fully read into a list.
+     * @return all result sets, update counts, iterators and OUT parameters of the execution, in the order they were
+     * produced.
      * @since 5.0
      */
-    QueryResponse onQuery(ObjectContext context, Query query, boolean iteratedResult);
+    List<QueryResultItem> onQuery(ObjectContext context, Query query, boolean iteratedResult);
 
     /**
      * Invalidates objects with the given ids in this channel and all its parents, so that they are refetched on the
diff --git a/cayenne/src/main/java/org/apache/cayenne/DataChannelQueryFilter.java b/cayenne/src/main/java/org/apache/cayenne/DataChannelQueryFilter.java
index 423f28963..3743f0cd5 100644
--- a/cayenne/src/main/java/org/apache/cayenne/DataChannelQueryFilter.java
+++ b/cayenne/src/main/java/org/apache/cayenne/DataChannelQueryFilter.java
@@ -21,17 +21,19 @@ package org.apache.cayenne;
 
 import org.apache.cayenne.query.Query;
 
+import java.util.List;
+
 /**
  * An interface of a filter that allows to intercept DataChannel query operations.
  * Query filters allow to implement chains of custom processors around a DataChannel.
  * <p>
  * Example: <pre>{@code
  * public class MyQueryFilter implements DataChannelQueryFilter {
- *     public QueryResponse onQuery(ObjectContext originatingContext, Query query, boolean iteratedResult,
- *                                  DataChannelQueryFilterChain filterChain) {
+ *     public List<QueryResultItem> onQuery(ObjectContext originatingContext, Query query, boolean iteratedResult,
+ *                                          DataChannelQueryFilterChain filterChain) {
  *         System.out.println("Do something before query");
  *         // process query or return some custom response
- *         QueryResponse response = filterChain.onQuery(originatingContext, query, iteratedResult);
+ *         List<QueryResultItem> response = filterChain.onQuery(originatingContext, query, iteratedResult);
  *         System.out.println("Do something after query");
  *         return response;
  *     }
@@ -50,9 +52,9 @@ public interface DataChannelQueryFilter {
      * @param query that is processed
      * @param iteratedResult whether the result should be returned as a {@link ResultIterator} instead of a list
      * @param filterChain chain of query filters to invoke after this filter
-     * @return query response
+     * @return query result items
      */
-    QueryResponse onQuery(ObjectContext originatingContext, Query query, boolean iteratedResult,
-                          DataChannelQueryFilterChain filterChain);
+    List<QueryResultItem> onQuery(ObjectContext originatingContext, Query query, boolean iteratedResult,
+                                  DataChannelQueryFilterChain filterChain);
 
 }
diff --git a/cayenne/src/main/java/org/apache/cayenne/DataChannelQueryFilterChain.java b/cayenne/src/main/java/org/apache/cayenne/DataChannelQueryFilterChain.java
index 02d0cfdcd..c21409be2 100644
--- a/cayenne/src/main/java/org/apache/cayenne/DataChannelQueryFilterChain.java
+++ b/cayenne/src/main/java/org/apache/cayenne/DataChannelQueryFilterChain.java
@@ -21,6 +21,8 @@ package org.apache.cayenne;
 
 import org.apache.cayenne.query.Query;
 
+import java.util.List;
+
 /**
  * Interface for chain of query filters
  *
@@ -28,6 +30,6 @@ import org.apache.cayenne.query.Query;
  */
 public interface DataChannelQueryFilterChain {
 
-    QueryResponse onQuery(ObjectContext originatingContext, Query query, boolean iteratedResult);
+    List<QueryResultItem> onQuery(ObjectContext originatingContext, Query query, boolean iteratedResult);
 
 }
diff --git a/cayenne/src/main/java/org/apache/cayenne/ObjectContext.java b/cayenne/src/main/java/org/apache/cayenne/ObjectContext.java
index a7057861d..e0a25bd13 100644
--- a/cayenne/src/main/java/org/apache/cayenne/ObjectContext.java
+++ b/cayenne/src/main/java/org/apache/cayenne/ObjectContext.java
@@ -349,10 +349,10 @@ public interface ObjectContext {
     <T> ResultBatchIterator<T> batchIterator(Select<T> query, int size);
 
     /**
-     * Executes any kind of query providing the result in a form of
-     * QueryResponse.
+     * Executes any kind of query, returning all of its result sets, update counts, iterators and OUT parameters as a
+     * list of {@link QueryResultItem}s in the order they were produced.
      */
-    QueryResponse performGenericQuery(Query query);
+    List<QueryResultItem> performGenericQuery(Query query);
 
     /**
      * Returns GraphManager that manages object graph associated with this
diff --git a/cayenne/src/main/java/org/apache/cayenne/QueryResponse.java b/cayenne/src/main/java/org/apache/cayenne/QueryResponse.java
deleted file mode 100644
index 456b7a8ef..000000000
--- a/cayenne/src/main/java/org/apache/cayenne/QueryResponse.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*****************************************************************
- *   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
- *
- *    https://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.cayenne;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * Represents a result of query execution. It potentially contain a mix of update counts
- * and lists of selected values. Provides API somewhat similar to java.util.Iterator or
- * java.sql.ResultSet for scanning through the individual results.
- * <p>
- * An example of iterating through a response:
- * </p>
- * 
- * <pre>
- * QueryResponse response = context.performGenericQuery(query);
- * for (response.reset(); response.next();) {
- *     if (response.isList()) {
- *         List list = response.currentList();
- *         // ...
- *     }
- *     else {
- *         int[] updateCounts = response.currentUpdateCount();
- *         // ...
- *     }
- * }
- * </pre>
- * 
- * <p>
- * In case the structure of the result is known, and only a single list or an update count
- * is expected, there is a simpler API to access them:
- * </p>
- * 
- * <pre>
- * QueryResponse response = context.performGenericQuery(query);
- * List list = response.firstList();
- * int[] count = response.firstUpdateCount();
- * </pre>
- * 
- * @since 1.2
- */
-public interface QueryResponse {
-
-    /**
-     * Returns a number of results in the response.
-     */
-    int size();
-
-    /**
-     * Returns whether current iteration result is a list or an update count.
-     */
-    default boolean isList() {
-        return false;
-    }
-
-    /**
-     * Returns whether current response  is an iterator
-     *
-     * @since 5.0
-     */
-    default boolean isIterator() {
-        return false;
-    }
-
-    /**
-     * Returns a List under the current iterator position. Use {@link #isList()} to check
-     * the result type before calling this method.
-     */
-    default List<?> currentList() {
-        return null;
-    }
-
-    /**
-     * Returns a current iterator.
-     *
-     * @since 5.0
-     */
-    default ResultIterator<?> currentIterator() {
-        return null;
-    }
-
-    /**
-     * Returns an update count under the current iterator position. Returned value is an
-     * int[] to accommodate batch queries. For a regular update result, the value will be
-     * an int[1]. Use {@link #isList()} to check the result type before calling this
-     * method.
-     */
-    default int[] currentUpdateCount() {
-        return null;
-    }
-
-    /**
-     * Returns whether the current iteration result is a map of stored procedure OUT parameters.
-     *
-     * @since 5.0
-     */
-    default boolean isOutParameters() {
-        return false;
-    }
-
-    /**
-     * Returns the current iteration result as a map of stored procedure OUT parameters keyed by name. Returns null
-     * unless {@link #isOutParameters()} is true.
-     *
-     * @since 5.0
-     */
-    default Map<String, ?> currentOutParameters() {
-        return null;
-    }
-
-    /**
-     * Rewinds response iterator to the next result, returning true if it is available.
-     */
-    boolean next();
-
-    /**
-     * Restarts response iterator.
-     */
-    void reset();
-
-    /**
-     * A utility method for quickly retrieving the first list in the response. Returns
-     * null if the query has no lists. Note that this method resets current iterator to an
-     * undefined state.
-     */
-    default List<?> firstList() {
-        return null;
-    }
-
-    /**
-     * A utility method for quickly retrieving the Iterator in the response. Returns
-     * null if the query has no iterator.
-     *
-     * @since 5.0
-     */
-    default ResultIterator<?> firstIterator() {
-        return null;
-    }
-
-    /**
-     * A utility method for quickly retrieving the first update count from the response.
-     * Note that this method resets current iterator to an undefined state.
-     */
-    default int[] firstUpdateCount() {
-        return null;
-    }
-}
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataContext.java b/cayenne/src/main/java/org/apache/cayenne/access/DataContext.java
index 9f6117d98..9c25346bc 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataContext.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataContext.java
@@ -28,7 +28,7 @@ import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.PersistenceState;
 import org.apache.cayenne.Persistent;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.ResultBatchIterator;
 import org.apache.cayenne.ResultIterator;
 import org.apache.cayenne.ResultIteratorCallback;
@@ -57,7 +57,6 @@ import org.apache.cayenne.reflect.PropertyVisitor;
 import org.apache.cayenne.reflect.ToManyProperty;
 import org.apache.cayenne.reflect.ToOneProperty;
 import org.apache.cayenne.util.EventUtil;
-import org.apache.cayenne.util.GenericResponse;
 import org.apache.cayenne.util.ShallowMergeOperation;
 import org.apache.cayenne.util.Util;
 
@@ -1018,28 +1017,28 @@ public class DataContext implements ObjectContext {
     @SuppressWarnings("unchecked")
     public <T> ResultIterator<T> iterator(Select<T> query) {
         Query queryToRun = nonNullDelegate().willPerformQuery(this, query);
-        QueryResponse queryResponse = onQuery(queryToRun, true, false);
-        return (ResultIterator<T>) queryResponse.firstIterator();
+        return (ResultIterator<T>) QueryResultItems.firstIterator(onQuery(queryToRun, true, false));
     }
 
     /**
-     * Executes a query returning a generic response.
+     * Executes a query returning all of its result sets, update counts, iterators and OUT parameters in the order
+     * they were produced.
      *
      * @since 1.2
      */
     @Override
-    public QueryResponse performGenericQuery(Query query) {
+    public List<QueryResultItem> performGenericQuery(Query query) {
 
         query = nonNullDelegate().willPerformGenericQuery(this, query);
         if (query == null) {
-            return new GenericResponse();
+            return Collections.emptyList();
         }
 
         if (this.getChannel() == null) {
             throw new CayenneRuntimeException("Can't run query - parent DataChannel is not set.");
         }
 
-        return onQuery(query, false, false);
+        return Collections.unmodifiableList(onQuery(query, false, false));
     }
 
     /**
@@ -1069,11 +1068,11 @@ public class DataContext implements ObjectContext {
             return new ArrayList<>(1);
         }
 
-        List<?> result = onQuery(query, false, false).firstList();
+        List<?> result = QueryResultItems.firstList(onQuery(query, false, false));
         return result != null ? result : new ArrayList<>(1);
     }
 
-    QueryResponse onQuery(Query query, boolean iteratedResult, boolean ignoreLocalCache) {
+    List<QueryResultItem> onQuery(Query query, boolean iteratedResult, boolean ignoreLocalCache) {
         return new DataContextQueryAction(this, query, iteratedResult, ignoreLocalCache).execute();
     }
 
@@ -1084,7 +1083,7 @@ public class DataContext implements ObjectContext {
      * @since 1.1
      */
     public int[] performNonSelectingQuery(Query query) {
-        int[] count = performGenericQuery(query).firstUpdateCount();
+        int[] count = QueryResultItems.firstUpdateCount(performGenericQuery(query));
         return count != null ? count : new int[0];
     }
 
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataContextChannel.java b/cayenne/src/main/java/org/apache/cayenne/access/DataContextChannel.java
index 5226093a0..c6b8529b9 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataContextChannel.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataContextChannel.java
@@ -24,7 +24,7 @@ import org.apache.cayenne.DataChannel;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.event.EventManager;
 import org.apache.cayenne.graph.ChildDiffLoader;
 import org.apache.cayenne.graph.CompoundDiff;
@@ -33,7 +33,6 @@ import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.EntityResultSegment;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.QueryMetadata;
-import org.apache.cayenne.util.GenericResponse;
 import org.apache.cayenne.util.ShallowMergeOperation;
 
 import java.util.ArrayList;
@@ -70,10 +69,10 @@ public record DataContextChannel(DataContext context) implements DataChannel {
     }
 
     @Override
-    public QueryResponse onQuery(ObjectContext childContext, Query query, boolean iteratedResult) {
+    public List<QueryResultItem> onQuery(ObjectContext childContext, Query query, boolean iteratedResult) {
         checkChildContext(childContext);
 
-        QueryResponse response = context.onQuery(query, iteratedResult, true);
+        List<QueryResultItem> response = context.onQuery(query, iteratedResult, true);
         QueryMetadata metadata = query.getMetaData(getEntityResolver());
 
         return metadata.isFetchingDataRows()
@@ -152,21 +151,21 @@ public record DataContextChannel(DataContext context) implements DataChannel {
         }
     }
 
-    private QueryResponse transferObjectsToChildContext(
+    private List<QueryResultItem> transferObjectsToChildContext(
             ObjectContext childContext,
-            QueryResponse response,
+            List<QueryResultItem> response,
             QueryMetadata metadata) {
 
         // rewrite response to contain objects from the query context
 
-        GenericResponse childResponse = new GenericResponse();
+        List<QueryResultItem> childResponse = new ArrayList<>(response.size());
         ShallowMergeOperation merger = null;
 
-        for (response.reset(); response.next(); ) {
-            if (response.isList()) {
-                List<?> objects = response.currentList();
+        for (QueryResultItem item : response) {
+            if (item instanceof QueryResultItem.Select<?> select) {
+                List<?> objects = select.objects();
                 if (objects.isEmpty()) {
-                    childResponse.addResultList(objects);
+                    childResponse.add(item);
                 } else {
 
                     // minor optimization, skip Object[] if there are no persistent objects
@@ -206,10 +205,10 @@ public record DataContextChannel(DataContext context) implements DataChannel {
                         }
                     }
 
-                    childResponse.addResultList(childObjects);
+                    childResponse.add(new QueryResultItem.Select<>(childObjects));
                 }
             } else {
-                childResponse.addBatchUpdateCount(response.currentUpdateCount());
+                childResponse.add(item);
             }
         }
 
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java b/cayenne/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java
index 749903d8e..33c2fc37a 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java
@@ -19,7 +19,7 @@
 
 package org.apache.cayenne.access;
 
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.cache.QueryCache;
 import org.apache.cayenne.cache.QueryCacheEntryFactory;
 import org.apache.cayenne.map.DbEntity;
@@ -27,7 +27,6 @@ import org.apache.cayenne.query.EntityResultSegment;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.QueryCacheStrategy;
 import org.apache.cayenne.query.QueryMetadata;
-import org.apache.cayenne.util.ListResponse;
 
 import java.util.Collections;
 import java.util.List;
@@ -42,7 +41,7 @@ class DataContextQueryAction {
     private final QueryMetadata metadata;
     private final boolean iteratedResult;
     private final boolean ignoreLocalCache;
-    private QueryResponse response;
+    private List<QueryResultItem> response;
 
     public DataContextQueryAction(DataContext context, Query query, boolean iteratedResult, boolean ignoreLocalCache) {
         this.context = context;
@@ -55,7 +54,7 @@ class DataContextQueryAction {
     /**
      * Worker method that performs internal query.
      */
-    public QueryResponse execute() {
+    public List<QueryResultItem> execute() {
         if (interceptIteratedQuery() != DONE) {
             if (interceptLocalCache() != DONE) {
                 executePostCache();
@@ -85,12 +84,12 @@ class DataContextQueryAction {
             // this will select raw ids
             runQuery();
 
-            List<?> rawIds = response.firstList();
+            List<?> rawIds = QueryResultItems.firstList(response);
             int maxIdQualifierSize = context.getChannel().getDataDomain().getMaxIdQualifierSize();
             IncrementalFaultList<?> paginatedList = createIncrementalFaultList(rawIds, maxIdQualifierSize);
 
             // replace result with a paginated list that will deal with id-to-object resolution
-            response = new ListResponse(paginatedList);
+            response = List.of(new QueryResultItem.Select<>(paginatedList));
             return DONE;
         }
 
@@ -153,12 +152,13 @@ class DataContextQueryAction {
 
         if (cache) {
             boolean wasResponseNull = (response == null);
-            List cachedResults = queryCache.get(metadata, factory);
+            List<?> cachedResults = queryCache.get(metadata, factory);
 
             // response may already be initialized by the factory above ... it is null if
             // there was a preexisting cache entry
+            // the cache may hold no list at all if the query produced no select result
             if (response == null || wasResponseNull) {
-                response = new ListResponse(cachedResults);
+                response = cachedResults != null ? List.of(new QueryResultItem.Select<>(cachedResults)) : List.of();
             }
         } else {
             // on cache-refresh request, fetch without blocking and fill the cache
@@ -175,7 +175,7 @@ class DataContextQueryAction {
     protected QueryCacheEntryFactory getCacheObjectFactory() {
         return () -> {
             executePostCache();
-            List result = response.firstList();
+            List<?> result = QueryResultItems.firstList(response);
             // make an immutable list to make sure callers don't mess it up
             return result != null ? Collections.unmodifiableList(result) : null;
         };
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataDomain.java b/cayenne/src/main/java/org/apache/cayenne/access/DataDomain.java
index a4b6cd513..b5c6b9227 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataDomain.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataDomain.java
@@ -28,7 +28,7 @@ import org.apache.cayenne.DataChannelSyncFilterChain;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.access.flush.DataDomainFlushAction;
 import org.apache.cayenne.access.flush.DataDomainFlushActionFactory;
 import org.apache.cayenne.cache.QueryCache;
@@ -344,12 +344,12 @@ public class DataDomain implements DataChannel {
     }
 
     /**
-     * Runs query returning generic QueryResponse.
+     * Runs a query, returning all of its results as a list of {@link QueryResultItem}s.
      *
      * @since 1.2
      */
     @Override
-    public QueryResponse onQuery(ObjectContext context, Query query, boolean iteratedResult) {
+    public List<QueryResultItem> onQuery(ObjectContext context, Query query, boolean iteratedResult) {
         checkStopped();
         return new DataDomainQueryFilterChain().onQuery(context, query, iteratedResult);
     }
@@ -405,7 +405,7 @@ public class DataDomain implements DataChannel {
         return new DataDomainRelationshipAction(this, context, sourceId, relationshipName).execute();
     }
 
-    QueryResponse onQueryNoFilters(ObjectContext originatingContext, Query query, boolean iteratedResult) {
+    List<QueryResultItem> onQueryNoFilters(ObjectContext originatingContext, Query query, boolean iteratedResult) {
         // transaction note:
         // we don't wrap this code in transaction to reduce transaction scope to
         // just the DB operation for better performance ... query action will
@@ -573,7 +573,7 @@ public class DataDomain implements DataChannel {
         }
 
         @Override
-        public QueryResponse onQuery(ObjectContext originatingContext, Query query, boolean iteratedResult) {
+        public List<QueryResultItem> onQuery(ObjectContext originatingContext, Query query, boolean iteratedResult) {
             return --idx >= 0
                     ? queryFilters.get(idx).onQuery(originatingContext, query, iteratedResult, this)
                     : onQueryNoFilters(originatingContext, query, iteratedResult);
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataDomainIdQueryAction.java b/cayenne/src/main/java/org/apache/cayenne/access/DataDomainIdQueryAction.java
index bb14bb49d..01c05e2f4 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataDomainIdQueryAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataDomainIdQueryAction.java
@@ -95,7 +95,7 @@ class DataDomainIdQueryAction {
                 .query(Persistent.class, id.getEntityName())
                 .where(ExpressionFactory.matchAllDbExp(id.getIdSnapshot(), Expression.EQUAL_TO));
 
-        List<?> objects = domain.onQuery(context, query, false).firstList();
+        List<?> objects = QueryResultItems.firstList(domain.onQuery(context, query, false));
         return switch (objects.size()) {
             case 0 -> null;
             case 1 -> (Persistent) objects.getFirst();
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java b/cayenne/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
index c618ea410..0129d5ee4 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
@@ -26,7 +26,7 @@ import org.apache.cayenne.EmbeddableObject;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.ResultIterator;
 import org.apache.cayenne.access.translator.TranslatedStatement;
 import org.apache.cayenne.cache.QueryCache;
@@ -51,8 +51,6 @@ import org.apache.cayenne.reflect.LifecycleCallbackRegistry;
 import org.apache.cayenne.tx.BaseTransaction;
 import org.apache.cayenne.tx.ReadOnlyTransaction;
 import org.apache.cayenne.tx.Transaction;
-import org.apache.cayenne.util.GenericResponse;
-import org.apache.cayenne.util.ListResponse;
 import org.apache.cayenne.util.Util;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -65,7 +63,6 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 import java.util.function.Function;
 
@@ -88,8 +85,8 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
     private final QueryMetadata metadata;
     private final AdhocObjectFactory objectFactory;
 
-    private QueryResponse response;
-    private GenericResponse fullResponse;
+    private List<QueryResultItem> response;
+    private List<QueryResultItem> fullResponse;
     private final boolean iteratedResult;
     private boolean iteratorExclusiveConnection;
     private Map<CayennePath, List<?>> prefetchResultsByPath;
@@ -105,7 +102,7 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
 
     /*
      * A constructor for the "new" way of performing a query via 'execute' with
-     * QueryResponse created internally.
+     * the result list created internally.
      */
     DataDomainQueryAction(ObjectContext context, DataDomain domain, Query query, boolean iteratedResult) {
         if (context != null && !(context instanceof DataContext)) {
@@ -121,7 +118,7 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
         this.objectFactory = domain.getObjectFactory();
     }
 
-    QueryResponse execute() {
+    List<QueryResultItem> execute() {
 
         // run chain...
         if (interceptIteratedQuery() != DONE) {
@@ -165,7 +162,7 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
         // The transaction is owned by the caller, and is not aligned with the iterator scope
         if (BaseTransaction.getThreadTransaction() != null) {
             runQuery();
-            Objects.requireNonNull(fullResponse.firstIterator(), "Iterator response expected");
+            firstIteratorIndex();
         }
 
         // The transaction will be owned by the iterator, and will outlive the query action
@@ -177,8 +174,9 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
                 // connection
                 iteratorExclusiveConnection = true;
                 runQuery();
-                ResultIterator<?> it = Objects.requireNonNull(fullResponse.firstIterator(), "Iterator response expected");
-                fullResponse.replaceResult(it, new TransactionResultIteratorDecorator<>(it, tx));
+                int index = firstIteratorIndex();
+                ResultIterator<?> it = ((QueryResultItem.Iterator<?>) fullResponse.get(index)).iterator();
+                fullResponse.set(index, new QueryResultItem.Iterator<>(new TransactionResultIteratorDecorator<>(it, tx)));
             } catch (Throwable th) {
                 // No iterator will be returned to the caller, so nothing will ever commit this transaction. Mark it
                 // for rollback below, so that its connections are not leaked. Note that a DataNode marks the
@@ -195,8 +193,15 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
                 }
             }
         }
+    }
 
-        fullResponse.reset();
+    private int firstIteratorIndex() {
+        for (int i = 0; i < fullResponse.size(); i++) {
+            if (fullResponse.get(i) instanceof QueryResultItem.Iterator) {
+                return i;
+            }
+        }
+        throw new CayenneRuntimeException("Iterator response expected");
     }
 
     /*
@@ -226,8 +231,12 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
             // it is null if there was a preexisting cache entry
             cacheHit = (response == null);
 
+            // the cache may hold no list at all if the query produced no select result
             if (response == null || wasResponseNull) {
-                response = new ListResponse(cachedResults);
+                response = new ArrayList<>(1);
+                if (cachedResults != null) {
+                    response.add(new QueryResultItem.Select<>(cachedResults));
+                }
             }
 
             // Mark as cached result - lists need copying whether hit or miss
@@ -250,7 +259,7 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
         return () -> {
             runQueryInTransaction();
 
-            List<?> list = response.firstList();
+            List<?> list = QueryResultItems.firstList(response);
             if (list != null) {
 
                 // make an immutable list to make sure callers don't mess it up
@@ -285,7 +294,7 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
 
     private void runQuery() {
         // reset
-        this.fullResponse = new GenericResponse();
+        this.fullResponse = new ArrayList<>();
         this.response = this.fullResponse;
         this.queriesByNode = null;
 
@@ -313,24 +322,23 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
         }
 
         ObjectConversionStrategy<?, ?> converter = getConverter();
-        // local copy because it can change while iterating
-        QueryResponse response = this.response;
-        for (response.reset(); response.next(); ) {
-            if (response.isList()) {
-                List<?> mainRows = response.currentList(); // List<DataRow> or List<Object[]>
-                if (mainRows != null && !mainRows.isEmpty()) {
-                    List<?> result = converter.convert((List) mainRows);
-                    if (result != mainRows) {
-                        updateResponse(mainRows, result);
+        for (int i = 0; i < response.size(); i++) {
+            switch (response.get(i)) {
+                case QueryResultItem.Select<?> select -> {
+                    List<?> mainRows = select.objects(); // List<DataRow> or List<Object[]>
+                    if (!mainRows.isEmpty()) {
+                        List<?> result = converter.convert((List) mainRows);
+                        if (result != mainRows) {
+                            response.set(i, new QueryResultItem.Select<>(result));
+                        }
                     }
                 }
-            } else if (response.isIterator()) {
-                // iterator should be a part of full response
-                ResultIterator<?> iterator = fullResponse.currentIterator();
-                fullResponse.replaceResult(iterator, new ResultIteratorConverterDecorator(iterator, converter));
+                case QueryResultItem.Iterator<?> iterator -> response.set(i, new QueryResultItem.Iterator<>(
+                        new ResultIteratorConverterDecorator(iterator.iterator(), converter)));
+                default -> {
+                }
             }
         }
-        response.reset();
     }
 
     private boolean noObjectConversion() {
@@ -424,12 +432,14 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
 
     @Override
     public void nextCount(Query query, int resultCount) {
-        fullResponse.addUpdateCount(resultCount);
+        fullResponse.add(new QueryResultItem.Update(new int[]{resultCount}));
     }
 
     @Override
     public void nextBatchCount(Query query, int[] resultCount) {
-        fullResponse.addBatchUpdateCount(resultCount);
+        if (resultCount != null) {
+            fullResponse.add(new QueryResultItem.Update(resultCount));
+        }
     }
 
     @Override
@@ -438,7 +448,7 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
         if (prefetchResultsByPath != null && query instanceof PrefetchSelectQuery<?> prefetchQuery) {
             prefetchResultsByPath.put(prefetchQuery.getPrefetchPath(), dataRows);
         } else {
-            fullResponse.addResultList(dataRows);
+            fullResponse.add(new QueryResultItem.Select<>(dataRows));
         }
     }
 
@@ -448,13 +458,13 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
         if (prefetchResultsByPath != null && query instanceof PrefetchSelectQuery<?> prefetchQuery) {
             prefetchResultsByPath.put(prefetchQuery.getPrefetchPath(), (List<?>) it);
         } else {
-            this.fullResponse.addResultIterator(it);
+            fullResponse.add(new QueryResultItem.Iterator<>(it));
         }
     }
 
     @Override
     public void nextOutParameters(Query query, Map<String, ?> outParameters) {
-        fullResponse.addOutParameters(outParameters);
+        fullResponse.add(new QueryResultItem.OutParameters(outParameters));
     }
 
     @Override
@@ -496,16 +506,6 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
         return iteratorExclusiveConnection;
     }
 
-    protected <T, R> void updateResponse(List<T> sourceObjects, List<? extends R> targetObjects) {
-        if (response instanceof GenericResponse genericResponse) {
-            genericResponse.replaceResult(sourceObjects, targetObjects);
-        } else if (response instanceof ListResponse) {
-            response = new ListResponse(targetObjects);
-        } else {
-            throw new IllegalStateException("Unknown response object: " + response);
-        }
-    }
-
     abstract class ObjectConversionStrategy<T, R> {
         abstract List<? extends R> convert(List<T> mainRows);
 
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataDomainRelationshipAction.java b/cayenne/src/main/java/org/apache/cayenne/access/DataDomainRelationshipAction.java
index cb1dffd0b..a7860c686 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataDomainRelationshipAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataDomainRelationshipAction.java
@@ -148,7 +148,7 @@ class DataDomainRelationshipAction {
                 .where(qualifier);
 
         @SuppressWarnings("unchecked")
-        List<Persistent> related = (List<Persistent>) domain.onQuery(context, query, false).firstList();
+        List<Persistent> related = (List<Persistent>) QueryResultItems.firstList(domain.onQuery(context, query, false));
         return related;
     }
 
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/ListWithPrefetches.java b/cayenne/src/main/java/org/apache/cayenne/access/ListWithPrefetches.java
index ee2c8c4cf..c2931b76d 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/ListWithPrefetches.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/ListWithPrefetches.java
@@ -37,7 +37,7 @@ import java.util.Map;
 // of changing DataRowStore result caching API. Since we are doing it when already in 1.2
 // beta, I am choosing the least invasive way that doesn't affect public API.
 //
-// Future alternatives may include caching the entire QueryResponse... or maybe leaving
+// Future alternatives may include caching the entire query result... or maybe leaving
 // everything the way it is.
 class ListWithPrefetches implements List<Object> {
 
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java b/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java
index 3155f6ee1..5fa119854 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java
@@ -495,7 +495,7 @@ public class ObjectStore implements SnapshotEventListener, GraphManager {
                 .where(ExpressionFactory.matchAllDbExp(oid.getIdSnapshot(), Expression.EQUAL_TO))
                 .fetchDataRows();
 
-        List<?> rows = context.getChannel().onQuery(context, query, false).firstList();
+        List<?> rows = QueryResultItems.firstList(context.getChannel().onQuery(context, query, false));
         return switch (rows.size()) {
             case 0 -> null;
             case 1 -> (DataRow) rows.getFirst();
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/QueryResultItems.java b/cayenne/src/main/java/org/apache/cayenne/access/QueryResultItems.java
new file mode 100644
index 000000000..bd25d855f
--- /dev/null
+++ b/cayenne/src/main/java/org/apache/cayenne/access/QueryResultItems.java
@@ -0,0 +1,71 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.access;
+
+import org.apache.cayenne.QueryResultItem;
+import org.apache.cayenne.ResultIterator;
+
+import java.util.List;
+
+/**
+ * Lookups of the first item of a given kind in a multipart query result.
+ *
+ * @since 5.0
+ */
+final class QueryResultItems {
+
+    private QueryResultItems() {
+    }
+
+    /**
+     * Returns the list of the first {@link QueryResultItem.Select} item, or null if there is none.
+     */
+    static List<?> firstList(List<QueryResultItem> items) {
+        for (QueryResultItem item : items) {
+            if (item instanceof QueryResultItem.Select<?> select) {
+                return select.objects();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the iterator of the first {@link QueryResultItem.Iterator} item, or null if there is none.
+     */
+    static ResultIterator<?> firstIterator(List<QueryResultItem> items) {
+        for (QueryResultItem item : items) {
+            if (item instanceof QueryResultItem.Iterator<?> iterator) {
+                return iterator.iterator();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the counts of the first {@link QueryResultItem.Update} item, or null if there is none.
+     */
+    static int[] firstUpdateCount(List<QueryResultItem> items) {
+        for (QueryResultItem item : items) {
+            if (item instanceof QueryResultItem.Update update) {
+                return update.counts();
+            }
+        }
+        return null;
+    }
+}
diff --git a/cayenne/src/main/java/org/apache/cayenne/query/MappedExec.java b/cayenne/src/main/java/org/apache/cayenne/query/MappedExec.java
index 57c1591e6..44e4ecce0 100644
--- a/cayenne/src/main/java/org/apache/cayenne/query/MappedExec.java
+++ b/cayenne/src/main/java/org/apache/cayenne/query/MappedExec.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.query;
 
+import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.QueryResultItem;
 
@@ -56,10 +57,20 @@ public class MappedExec extends AbstractMappedQuery {
      * were produced.
      */
     public List<QueryResultItem> execute(ObjectContext context) {
-        return QueryResultItems.fromResponse(context.performGenericQuery(this));
+        return context.performGenericQuery(this);
     }
 
+    /**
+     * Executes the query, returning the counts of its first update result.
+     *
+     * @throws CayenneRuntimeException if the query produced no update results.
+     */
     public int[] update(ObjectContext context) {
-        return context.performGenericQuery(this).firstUpdateCount();
+        for (QueryResultItem item : execute(context)) {
+            if (item instanceof QueryResultItem.Update update) {
+                return update.counts();
+            }
+        }
+        throw new CayenneRuntimeException("Expected an update result from query '%s'", queryName);
     }
 }
diff --git a/cayenne/src/main/java/org/apache/cayenne/query/ProcedureCall.java b/cayenne/src/main/java/org/apache/cayenne/query/ProcedureCall.java
index dbb8c0e1b..6e66768e0 100644
--- a/cayenne/src/main/java/org/apache/cayenne/query/ProcedureCall.java
+++ b/cayenne/src/main/java/org/apache/cayenne/query/ProcedureCall.java
@@ -134,7 +134,7 @@ public class ProcedureCall<T> extends IndirectQuery {
      * were produced.
      */
     public List<QueryResultItem> call(ObjectContext context) {
-        return QueryResultItems.fromResponse(context.performGenericQuery(this));
+        return context.performGenericQuery(this);
     }
 
     /**
diff --git a/cayenne/src/main/java/org/apache/cayenne/query/ProcedureQuery.java b/cayenne/src/main/java/org/apache/cayenne/query/ProcedureQuery.java
index d9e961097..5bd4ade59 100644
--- a/cayenne/src/main/java/org/apache/cayenne/query/ProcedureQuery.java
+++ b/cayenne/src/main/java/org/apache/cayenne/query/ProcedureQuery.java
@@ -19,6 +19,7 @@
 
 package org.apache.cayenne.query;
 
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.map.DataMap;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.EntityResolver;
@@ -41,7 +42,7 @@ import java.util.Map;
  * <h4>Reading OUT parameters</h4>
  * <p>
  * If a ProcedureQuery has OUT parameters, their values are reported as a separate item of the query response, a Map
- * keyed by parameter name (see {@link org.apache.cayenne.QueryResponse#currentOutParameters()}).
+ * keyed by parameter name (see {@link QueryResultItem.OutParameters}).
  * </p>
  * <h4>Using ProcedureQuery as a GenericSelectQuery</h4>
  * <p>
diff --git a/cayenne/src/main/java/org/apache/cayenne/query/QueryResultItems.java b/cayenne/src/main/java/org/apache/cayenne/query/QueryResultItems.java
deleted file mode 100644
index b75f98ec0..000000000
--- a/cayenne/src/main/java/org/apache/cayenne/query/QueryResultItems.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*****************************************************************
- *   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
- *
- *    https://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.cayenne.query;
-
-import org.apache.cayenne.QueryResponse;
-import org.apache.cayenne.QueryResultItem;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Converts a {@link QueryResponse} into a list of {@link QueryResultItem}s.
- *
- * @since 5.0
- */
-final class QueryResultItems {
-
-    private QueryResultItems() {
-    }
-
-    static List<QueryResultItem> fromResponse(QueryResponse response) {
-        List<QueryResultItem> items = new ArrayList<>(response.size());
-        for (response.reset(); response.next(); ) {
-            if (response.isList()) {
-                items.add(new QueryResultItem.Select<>(response.currentList()));
-            } else if (response.isIterator()) {
-                items.add(new QueryResultItem.Iterator<>(response.currentIterator()));
-            } else if (response.isOutParameters()) {
-                items.add(new QueryResultItem.OutParameters(response.currentOutParameters()));
-            } else {
-                items.add(new QueryResultItem.Update(response.currentUpdateCount()));
-            }
-        }
-        return items;
-    }
-}
diff --git a/cayenne/src/main/java/org/apache/cayenne/query/SQLExec.java b/cayenne/src/main/java/org/apache/cayenne/query/SQLExec.java
index 62d0a4730..e426d8b25 100644
--- a/cayenne/src/main/java/org/apache/cayenne/query/SQLExec.java
+++ b/cayenne/src/main/java/org/apache/cayenne/query/SQLExec.java
@@ -157,7 +157,7 @@ public class SQLExec extends IndirectQuery {
      * Executes the query, returning all of its result sets and update counts in the order they were produced.
      */
     public List<QueryResultItem> execute(ObjectContext context) {
-        return QueryResultItems.fromResponse(context.performGenericQuery(this));
+        return context.performGenericQuery(this);
     }
 
     public int update(ObjectContext context) {
@@ -192,7 +192,8 @@ public class SQLExec extends IndirectQuery {
 
     /**
      * Flag indicating that generated keys should be returned by this query execution.
-     * Generated keys could be read via {@link QueryResponse#currentList()} method
+     * Generated keys are returned as a {@link QueryResultItem.Select} item of {@link #execute(ObjectContext)}
+     * result.
      *
      * @param returnGeneratedKeys flag value
      * @see java.sql.Statement#RETURN_GENERATED_KEYS
diff --git a/cayenne/src/main/java/org/apache/cayenne/util/GenericResponse.java b/cayenne/src/main/java/org/apache/cayenne/util/GenericResponse.java
deleted file mode 100644
index 6fc721f77..000000000
--- a/cayenne/src/main/java/org/apache/cayenne/util/GenericResponse.java
+++ /dev/null
@@ -1,205 +0,0 @@
-/*****************************************************************
- *   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
- *
- *    https://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.cayenne.util;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.cayenne.QueryResponse;
-import org.apache.cayenne.ResultIterator;
-
-/**
- * A simple implementation of QueryResponse.
- *
- * @since 1.2
- */
-public class GenericResponse implements QueryResponse {
-
-    protected List<Object> results;
-
-    protected int currentIndex;
-
-    /**
-     * Creates an empty BaseResponse.
-     */
-    public GenericResponse() {
-        results = new ArrayList<>();
-    }
-
-    /**
-     * Creates a BaseResponse with a single result list.
-     */
-    public GenericResponse(List<?> list) {
-        results = new ArrayList<>(1);
-        addResultList(list);
-    }
-
-    /**
-     * Creates a response that it a shallow copy of another response.
-     */
-    @SuppressWarnings("unused")
-    public GenericResponse(QueryResponse response) {
-        results = new ArrayList<>(response.size());
-
-        response.reset();
-        while (response.next()) {
-            if (response.isList()) {
-                addResultList(response.currentList());
-            } else if (response.isIterator()) {
-                addResultIterator(response.currentIterator());
-            } else if (response.isOutParameters()) {
-                addOutParameters(response.currentOutParameters());
-            } else {
-                addBatchUpdateCount(response.currentUpdateCount());
-            }
-        }
-    }
-
-    @Override
-    public List<?> firstList() {
-        for (reset(); next(); ) {
-            if (isList()) {
-                return currentList();
-            }
-        }
-
-        return null;
-    }
-
-    @Override
-    public ResultIterator<?> firstIterator() {
-        for (reset(); next(); ) {
-            if (isIterator()) {
-                return currentIterator();
-            }
-        }
-
-        return null;
-    }
-
-    @Override
-    public int[] firstUpdateCount() {
-        for (reset(); next(); ) {
-            if (results.get(currentIndex - 1) instanceof int[] counts) {
-                return counts;
-            }
-        }
-
-        return null;
-    }
-
-    @Override
-    public List<?> currentList() {
-        return (List<?>) results.get(currentIndex - 1);
-    }
-
-    @Override
-    public ResultIterator<?> currentIterator() {
-        return (ResultIterator<?>) results.get(currentIndex - 1);
-    }
-
-    @Override
-    public int[] currentUpdateCount() {
-        return (int[]) results.get(currentIndex - 1);
-    }
-
-    @SuppressWarnings("unchecked")
-    @Override
-    public Map<String, ?> currentOutParameters() {
-        return (Map<String, ?>) results.get(currentIndex - 1);
-    }
-
-    @Override
-    public boolean isList() {
-        return results.get(currentIndex - 1) instanceof List;
-    }
-
-    @Override
-    public boolean isOutParameters() {
-        return results.get(currentIndex - 1) instanceof Map;
-    }
-
-    @Override
-    public boolean isIterator() {
-        return results.get(currentIndex - 1) instanceof ResultIterator;
-    }
-
-    @Override
-    public boolean next() {
-        return ++currentIndex <= results.size();
-    }
-
-    @Override
-    public void reset() {
-        currentIndex = 0;
-    }
-
-    @Override
-    public int size() {
-        return results.size();
-    }
-
-    /**
-     * Clears any previously collected information.
-     */
-    public void clear() {
-        results.clear();
-    }
-
-    public void addBatchUpdateCount(int[] resultCount) {
-        if (resultCount != null) {
-            results.add(resultCount);
-        }
-    }
-
-    public void addUpdateCount(int resultCount) {
-        results.add(new int[]{resultCount});
-    }
-
-    public void addResultList(List<?> list) {
-        results.add(list);
-    }
-
-    /**
-     * @param iterator to add as a result
-     * @since 5.0
-     */
-    public void addResultIterator(ResultIterator<?> iterator) {
-        results.add(iterator);
-    }
-
-    /**
-     * @since 5.0
-     */
-    public void addOutParameters(Map<String, ?> outParameters) {
-        results.add(outParameters);
-    }
-
-    /**
-     * Replaces previously stored result with a new result.
-     */
-    public void replaceResult(Object oldResult, Object newResult) {
-        int index = results.indexOf(oldResult);
-        if (index >= 0) {
-            results.set(index, newResult);
-        }
-    }
-}
diff --git a/cayenne/src/main/java/org/apache/cayenne/util/ListResponse.java b/cayenne/src/main/java/org/apache/cayenne/util/ListResponse.java
deleted file mode 100644
index cd6e0a23b..000000000
--- a/cayenne/src/main/java/org/apache/cayenne/util/ListResponse.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*****************************************************************
- *   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
- *
- *    https://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.cayenne.util;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import org.apache.cayenne.QueryResponse;
-
-/**
- * A QueryResponse optimized to hold a single object or data row list.
- * 
- * @since 1.2
- */
-public class ListResponse implements QueryResponse {
-
-    protected List<?> objectList;
-
-    protected int currentIndex;
-
-    /**
-     * Creates an empty response.
-     */
-    public ListResponse() {
-        this.objectList = new ArrayList<>(1);
-    }
-
-    public ListResponse(Object object) {
-        this.objectList = Collections.singletonList(object);
-    }
-
-    public ListResponse(List<?> objectList) {
-        this.objectList = objectList;
-    }
-
-    public int size() {
-        return 1;
-    }
-
-    public boolean isList() {
-        if (currentIndex != 1) {
-            throw new IndexOutOfBoundsException("Past iteration end: " + currentIndex);
-        }
-
-        return true;
-    }
-
-    public List<?> currentList() {
-        if (currentIndex != 1) {
-            throw new IndexOutOfBoundsException("Past iteration end: " + currentIndex);
-        }
-
-        return objectList;
-    }
-
-    public int[] currentUpdateCount() {
-        throw new IllegalStateException("Current object is not an update count");
-    }
-
-    public boolean next() {
-        return ++currentIndex <= 1;
-    }
-
-    public void reset() {
-        currentIndex = 0;
-    }
-
-    public List<?> firstList() {
-        return objectList;
-    }
-
-}
diff --git a/cayenne/src/test/java/org/apache/cayenne/MockDataChannel.java b/cayenne/src/test/java/org/apache/cayenne/MockDataChannel.java
index 168117d81..4d77020d0 100644
--- a/cayenne/src/test/java/org/apache/cayenne/MockDataChannel.java
+++ b/cayenne/src/test/java/org/apache/cayenne/MockDataChannel.java
@@ -23,14 +23,10 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 
-import org.apache.cayenne.DataChannel;
-import org.apache.cayenne.ObjectContext;
-import org.apache.cayenne.QueryResponse;
 import org.apache.cayenne.event.EventManager;
 import org.apache.cayenne.graph.GraphDiff;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.Query;
-import org.apache.cayenne.util.GenericResponse;
 
 /**
  * Stores all messages passed via this handler.
@@ -41,7 +37,7 @@ public class MockDataChannel implements DataChannel {
     protected EntityResolver resolver;
     protected List requestObjects = new ArrayList();
     protected GraphDiff commitResponse;
-    protected QueryResponse response;
+    protected List<QueryResultItem> response;
 
     public MockDataChannel() {
 
@@ -52,7 +48,7 @@ public class MockDataChannel implements DataChannel {
     }
 
     public MockDataChannel(List selectResponse) {
-        this.response = new GenericResponse(selectResponse);
+        this.response = List.of(new QueryResultItem.Select<>(selectResponse));
     }
 
     public MockDataChannel(EntityResolver entityResolver, List selectResponse) {
@@ -60,13 +56,9 @@ public class MockDataChannel implements DataChannel {
         this.resolver = entityResolver;
     }
 
-    public MockDataChannel(EntityResolver entityResolver, QueryResponse response) {
-        this.resolver = entityResolver;
-        this.response = response;
-    }
-
     public MockDataChannel(EntityResolver resolver) {
-        this(resolver, new GenericResponse());
+        this.resolver = resolver;
+        this.response = new ArrayList<>();
     }
 
     public EventManager getEventManager() {
@@ -90,7 +82,7 @@ public class MockDataChannel implements DataChannel {
         return commitResponse;
     }
 
-    public QueryResponse onQuery(ObjectContext context, Query query, boolean iteratedResult) {
+    public List<QueryResultItem> onQuery(ObjectContext context, Query query, boolean iteratedResult) {
         requestObjects.add(query);
         return response;
     }
@@ -101,14 +93,23 @@ public class MockDataChannel implements DataChannel {
 
     public Persistent onIdQuery(ObjectContext context, ObjectId id) {
         requestObjects.add(id);
-        List<?> objects = response != null ? response.firstList() : null;
+        List<?> objects = response != null ? firstList() : null;
         return objects == null || objects.isEmpty() ? null : (Persistent) objects.getFirst();
     }
 
     public List<Persistent> onRelationshipQuery(ObjectContext context, ObjectId sourceId,
                                                 String relationshipName) {
         requestObjects.add(sourceId);
-        return (List<Persistent>) response.firstList();
+        return (List<Persistent>) firstList();
+    }
+
+    private List<?> firstList() {
+        for (QueryResultItem item : response) {
+            if (item instanceof QueryResultItem.Select<?> select) {
+                return select.objects();
+            }
+        }
+        return null;
     }
 
     public EntityResolver getEntityResolver() {
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextDecoratedStackIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextDecoratedStackIT.java
index 199dbad90..48f051742 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextDecoratedStackIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextDecoratedStackIT.java
@@ -23,7 +23,7 @@ import org.apache.cayenne.DataChannel;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.dba.frontbase.FrontBaseAdapter;
 import org.apache.cayenne.dba.mysql.MySQLAdapter;
 import org.apache.cayenne.event.EventManager;
@@ -114,7 +114,7 @@ public class DataContextDecoratedStackIT {
         }
 
         @Override
-        public QueryResponse onQuery(ObjectContext context, Query query, boolean iteratedResult) {
+        public List<QueryResultItem> onQuery(ObjectContext context, Query query, boolean iteratedResult) {
             return channel.onQuery(context, query, iteratedResult);
         }
 
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLDeleteIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLDeleteIT.java
index 299a927c4..2b3f81829 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLDeleteIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLDeleteIT.java
@@ -20,7 +20,7 @@ package org.apache.cayenne.access;
 
 import org.apache.cayenne.Cayenne;
 import org.apache.cayenne.ObjectContext;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.query.EJBQLQuery;
 import org.apache.cayenne.runtime.CayenneRuntime;
 import org.apache.cayenne.test.jdbc.TableHelper;
@@ -32,6 +32,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 import java.sql.Types;
+import java.util.List;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -72,9 +73,9 @@ public class DataContextEJBQLDeleteIT {
         String ejbql = "delete from Painting";
         EJBQLQuery query = new EJBQLQuery(ejbql);
 
-        QueryResponse result = env.context().performGenericQuery(query);
+        List<QueryResultItem> result = env.context().performGenericQuery(query);
 
-        int[] count = result.firstUpdateCount();
+        int[] count = ((QueryResultItem.Update) result.getFirst()).counts();
         assertNotNull(count);
         assertEquals(1, count.length);
         assertEquals(2, count[0]);
@@ -87,9 +88,9 @@ public class DataContextEJBQLDeleteIT {
         String ejbql = "delete from Painting AS p";
         EJBQLQuery query = new EJBQLQuery(ejbql);
 
-        QueryResponse result = env.context().performGenericQuery(query);
+        List<QueryResultItem> result = env.context().performGenericQuery(query);
 
-        int[] count = result.firstUpdateCount();
+        int[] count = ((QueryResultItem.Update) result.getFirst()).counts();
         assertNotNull(count);
         assertEquals(1, count.length);
         assertEquals(2, count[0]);
@@ -102,9 +103,9 @@ public class DataContextEJBQLDeleteIT {
         String ejbql = "delete from Painting AS p WHERE p.paintingTitle = 'P2'";
         EJBQLQuery query = new EJBQLQuery(ejbql);
 
-        QueryResponse result = env.context().performGenericQuery(query);
+        List<QueryResultItem> result = env.context().performGenericQuery(query);
 
-        int[] count = result.firstUpdateCount();
+        int[] count = ((QueryResultItem.Update) result.getFirst()).counts();
         assertNotNull(count);
         assertEquals(1, count.length);
         assertEquals(1, count[0]);
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLDeletePKIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLDeletePKIT.java
index 601802ee3..a771f8116 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLDeletePKIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLDeletePKIT.java
@@ -18,7 +18,7 @@
  ****************************************************************/
 
 package org.apache.cayenne.access;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.query.EJBQLQuery;
 import org.apache.cayenne.test.jdbc.TableHelper;
 import org.apache.cayenne.unit.CayenneProjects;
@@ -64,9 +64,9 @@ public class DataContextEJBQLDeletePKIT {
 
         EJBQLQuery query = new EJBQLQuery(ejbql);
         query.setParameter("id", id);
-        QueryResponse result = env.context().performGenericQuery(query);
+        List<QueryResultItem> result = env.context().performGenericQuery(query);
 
-        int[] count = result.firstUpdateCount();
+        int[] count = ((QueryResultItem.Update) result.getFirst()).counts();
         assertNotNull(count);
         assertEquals(1, count.length);
         assertEquals(33, count[0]);
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLNumericalFunctionalIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLNumericalFunctionalIT.java
index b375a86b9..c13b33224 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLNumericalFunctionalIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLNumericalFunctionalIT.java
@@ -24,7 +24,7 @@ import java.math.BigInteger;
 import java.util.List;
 
 import org.apache.cayenne.Cayenne;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.query.EJBQLQuery;
 import org.apache.cayenne.test.jdbc.TableHelper;
 import org.apache.cayenne.testdo.numeric_types.BigDecimalEntity;
@@ -130,9 +130,9 @@ public class DataContextEJBQLNumericalFunctionalIT {
         String ejbql = "UPDATE BooleanTestEntity AS p SET p.booleanColumn = true";
         EJBQLQuery query = new EJBQLQuery(ejbql);
 
-        QueryResponse result = env.context().performGenericQuery(query);
+        List<QueryResultItem> result = env.context().performGenericQuery(query);
 
-        int[] count = result.firstUpdateCount();
+        int[] count = ((QueryResultItem.Update) result.getFirst()).counts();
         assertNotNull(count);
         assertEquals(1, count.length);
         assertEquals(3, count[0]);
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLUpdateCompoundIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLUpdateCompoundIT.java
index 75bf99ea5..7b322964f 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLUpdateCompoundIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLUpdateCompoundIT.java
@@ -20,7 +20,7 @@
 package org.apache.cayenne.access;
 
 import org.apache.cayenne.Cayenne;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.query.EJBQLQuery;
 import org.apache.cayenne.test.jdbc.TableHelper;
 import org.apache.cayenne.testdo.compound.CompoundPkTestEntity;
@@ -31,6 +31,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -82,9 +83,9 @@ public class DataContextEJBQLUpdateCompoundIT {
         EJBQLQuery query = new EJBQLQuery(ejbql);
         query.setParameter("param", object);
 
-        QueryResponse result = env.context().performGenericQuery(query);
+        List<QueryResultItem> result = env.context().performGenericQuery(query);
 
-        int[] count = result.firstUpdateCount();
+        int[] count = ((QueryResultItem.Update) result.getFirst()).counts();
         assertNotNull(count);
         assertEquals(1, count.length);
         assertEquals(2, count[0]);
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLUpdateIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLUpdateIT.java
index b8ec71639..27f3460cf 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLUpdateIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextEJBQLUpdateIT.java
@@ -19,7 +19,7 @@
 package org.apache.cayenne.access;
 
 import org.apache.cayenne.Cayenne;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.query.EJBQLQuery;
 import org.apache.cayenne.test.jdbc.TableHelper;
 import org.apache.cayenne.testdo.testmap.Artist;
@@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 import java.sql.Types;
+import java.util.List;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -80,9 +81,9 @@ public class DataContextEJBQLUpdateIT {
         String ejbql = "UPDATE Painting AS p SET p.paintingTitle = 'XX' WHERE p.paintingTitle = 'P1'";
         EJBQLQuery query = new EJBQLQuery(ejbql);
 
-        QueryResponse result = env.context().performGenericQuery(query);
+        List<QueryResultItem> result = env.context().performGenericQuery(query);
 
-        int[] count = result.firstUpdateCount();
+        int[] count = ((QueryResultItem.Update) result.getFirst()).counts();
         assertNotNull(count);
         assertEquals(1, count.length);
         assertEquals(1, count[0]);
@@ -104,9 +105,9 @@ public class DataContextEJBQLUpdateIT {
         String ejbql = "UPDATE Painting AS p SET p.paintingTitle = 'XX'";
         EJBQLQuery query = new EJBQLQuery(ejbql);
 
-        QueryResponse result = env.context().performGenericQuery(query);
+        List<QueryResultItem> result = env.context().performGenericQuery(query);
 
-        int[] count = result.firstUpdateCount();
+        int[] count = ((QueryResultItem.Update) result.getFirst()).counts();
         assertNotNull(count);
         assertEquals(1, count.length);
         assertEquals(2, count[0]);
@@ -128,9 +129,9 @@ public class DataContextEJBQLUpdateIT {
         String ejbql = "UPDATE Painting AS p SET p.estimatedPrice = NULL";
         EJBQLQuery query = new EJBQLQuery(ejbql);
 
-        QueryResponse result = env.context().performGenericQuery(query);
+        List<QueryResultItem> result = env.context().performGenericQuery(query);
 
-        int[] count = result.firstUpdateCount();
+        int[] count = ((QueryResultItem.Update) result.getFirst()).counts();
         assertNotNull(count);
         assertEquals(1, count.length);
         assertEquals(2, count[0]);
@@ -154,9 +155,9 @@ public class DataContextEJBQLUpdateIT {
     // String ejbql = "UPDATE Painting AS p SET p.estimatedPrice = p.estimatedPrice * 2";
     // EJBQLQuery query = new EJBQLQuery(ejbql);
     //
-    // QueryResponse result = context.performGenericQuery(query);
+    // List<QueryResultItem> result = context.performGenericQuery(query);
     //
-    // int[] count = result.firstUpdateCount();
+    // int[] count = ((QueryResultItem.Update) result.getFirst()).counts();
     // assertNotNull(count);
     // assertEquals(1, count.length);
     // assertEquals(2, count[0]);
@@ -178,9 +179,9 @@ public class DataContextEJBQLUpdateIT {
         String ejbql = "UPDATE Painting AS p SET p.paintingTitle = 'XX', p.estimatedPrice = 1";
         EJBQLQuery query = new EJBQLQuery(ejbql);
 
-        QueryResponse result = env.context().performGenericQuery(query);
+        List<QueryResultItem> result = env.context().performGenericQuery(query);
 
-        int[] count = result.firstUpdateCount();
+        int[] count = ((QueryResultItem.Update) result.getFirst()).counts();
         assertNotNull(count);
         assertEquals(1, count.length);
         assertEquals(2, count[0]);
@@ -202,9 +203,9 @@ public class DataContextEJBQLUpdateIT {
         String ejbql = "UPDATE Painting AS p SET p.estimatedPrice = 1.1";
         EJBQLQuery query = new EJBQLQuery(ejbql);
 
-        QueryResponse result = env.context().performGenericQuery(query);
+        List<QueryResultItem> result = env.context().performGenericQuery(query);
 
-        int[] count = result.firstUpdateCount();
+        int[] count = ((QueryResultItem.Update) result.getFirst()).counts();
         assertNotNull(count);
         assertEquals(1, count.length);
         assertEquals(2, count[0]);
@@ -230,9 +231,9 @@ public class DataContextEJBQLUpdateIT {
         EJBQLQuery query = new EJBQLQuery(ejbql);
         query.setParameter("artist", object);
 
-        QueryResponse result = env.context().performGenericQuery(query);
+        List<QueryResultItem> result = env.context().performGenericQuery(query);
 
-        int[] count = result.firstUpdateCount();
+        int[] count = ((QueryResultItem.Update) result.getFirst()).counts();
         assertNotNull(count);
         assertEquals(1, count.length);
         assertEquals(2, count[0]);
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextProcedureQueryIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextProcedureQueryIT.java
index 5d98c30d2..66fa01b42 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextProcedureQueryIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextProcedureQueryIT.java
@@ -20,7 +20,7 @@
 package org.apache.cayenne.access;
 
 import org.apache.cayenne.DataRow;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.log.NoopSQLLogger;
 import org.apache.cayenne.map.Procedure;
@@ -288,12 +288,12 @@ public class DataContextProcedureQueryIT  {
         ProcedureQuery q = new ProcedureQuery(OUT_STORED_PROCEDURE);
         q.addParameter("in_param", 20);
 
-        QueryResponse response = runProcedureGeneric(q);
+        List<QueryResultItem> response = runProcedureGeneric(q);
 
         Map<String, ?> outParams = null;
-        for (response.reset(); response.next(); ) {
-            if (response.isOutParameters()) {
-                outParams = response.currentOutParameters();
+        for (QueryResultItem item : response) {
+            if (item instanceof QueryResultItem.OutParameters out) {
+                outParams = out.values();
             }
         }
 
@@ -375,7 +375,7 @@ public class DataContextProcedureQueryIT  {
         assertTrue(id instanceof Long, "Expected Long, got: " + id.getClass().getName());
     }
 
-    protected QueryResponse runProcedureGeneric(ProcedureQuery q) throws Exception {
+    protected List<QueryResultItem> runProcedureGeneric(ProcedureQuery q) throws Exception {
         BaseTransaction t = new ExternalTransaction(NoopSQLLogger.getInstance());
         BaseTransaction.bindThreadTransaction(t);
 
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataDomainFiltersIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataDomainFiltersIT.java
index abba0dbc1..36cd4895d 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataDomainFiltersIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataDomainFiltersIT.java
@@ -26,7 +26,7 @@ import org.apache.cayenne.DataChannelQueryFilterChain;
 import org.apache.cayenne.DataChannelSyncFilter;
 import org.apache.cayenne.DataChannelSyncFilterChain;
 import org.apache.cayenne.ObjectContext;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.annotation.PostPersist;
 import org.apache.cayenne.graph.GraphDiff;
 import org.apache.cayenne.query.ObjectSelect;
@@ -35,7 +35,6 @@ import org.apache.cayenne.testdo.testmap.Artist;
 import org.apache.cayenne.unit.CayenneProjects;
 import org.apache.cayenne.unit.CayenneTestsEnv;
 import org.junit.jupiter.api.extension.RegisterExtension;
-import org.apache.cayenne.util.ListResponse;
 import org.junit.jupiter.api.Test;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -62,14 +61,14 @@ public class DataDomainFiltersIT {
 
         DataChannelQueryFilter f1 = (originatingContext, query, iteratedResult, filterChain) -> {
             results.add("f1start");
-            QueryResponse response = filterChain.onQuery(originatingContext, query, iteratedResult);
+            List<QueryResultItem> response = filterChain.onQuery(originatingContext, query, iteratedResult);
             results.add("f1end");
             return response;
         };
 
         DataChannelQueryFilter f2 = (originatingContext, query, iteratedResult, filterChain) -> {
             results.add("f2start");
-            QueryResponse response = filterChain.onQuery(originatingContext, query, iteratedResult);
+            List<QueryResultItem> response = filterChain.onQuery(originatingContext, query, iteratedResult);
             results.add("f2end");
             return response;
         };
@@ -78,7 +77,7 @@ public class DataDomainFiltersIT {
         domain.queryFilters.add(f2);
 
         ObjectSelect<Artist> query = ObjectSelect.query(Artist.class);
-        QueryResponse response = domain.onQuery(env.context(), query, false);
+        List<QueryResultItem> response = domain.onQuery(env.context(), query, false);
         assertNotNull(response);
         assertEquals(4, results.size());
         assertEquals("f2start", results.get(0));
@@ -128,8 +127,8 @@ public class DataDomainFiltersIT {
 
         DataDomain domain = env.runtime().getDataDomain();
 
-        QueryResponse r1 = new ListResponse();
-        QueryResponse r2 = new ListResponse();
+        List<QueryResultItem> r1 = new ArrayList<>();
+        List<QueryResultItem> r2 = new ArrayList<>();
 
         DataChannelQueryFilter f1 = (originatingContext, query, iteratedResult, filterChain) -> r1;
         DataChannelQueryFilter f2 = (originatingContext, query, iteratedResult, filterChain) -> r2;
@@ -138,7 +137,7 @@ public class DataDomainFiltersIT {
         domain.queryFilters.add(f2);
 
         ObjectSelect<Artist> query = ObjectSelect.query(Artist.class);
-        QueryResponse response = domain.onQuery(env.context(), query, false);
+        List<QueryResultItem> response = domain.onQuery(env.context(), query, false);
 
         assertSame(r2, response);
     }
@@ -167,8 +166,8 @@ public class DataDomainFiltersIT {
         private List<String> results = new ArrayList<>();
 
         @Override
-        public QueryResponse onQuery(ObjectContext originatingContext, Query query, boolean iteratedResult,
-                                     DataChannelQueryFilterChain filterChain) {
+        public List<QueryResultItem> onQuery(ObjectContext originatingContext, Query query, boolean iteratedResult,
+                                             DataChannelQueryFilterChain filterChain) {
             results.add("onQuery");
             return filterChain.onQuery(originatingContext, query, iteratedResult);
         }
diff --git a/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataDomainProviderIT.java b/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataDomainProviderIT.java
index 8244e6303..d49a9cabe 100644
--- a/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataDomainProviderIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataDomainProviderIT.java
@@ -23,7 +23,7 @@ import org.apache.cayenne.DataChannelQueryFilterChain;
 import org.apache.cayenne.DataChannelSyncFilter;
 import org.apache.cayenne.DataChannelSyncFilterChain;
 import org.apache.cayenne.ObjectContext;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.access.DataDomain;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.dbsync.SkipSchemaUpdateStrategy;
@@ -44,6 +44,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 import javax.sql.DataSource;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -249,8 +250,8 @@ public class DataDomainProviderIT {
     static class NoopQueryFilter implements DataChannelQueryFilter {
 
         @Override
-        public QueryResponse onQuery(ObjectContext originatingContext, Query query, boolean iteratedResult,
-                                     DataChannelQueryFilterChain filterChain) {
+        public List<QueryResultItem> onQuery(ObjectContext originatingContext, Query query, boolean iteratedResult,
+                                             DataChannelQueryFilterChain filterChain) {
             return filterChain.onQuery(originatingContext, query, iteratedResult);
         }
     }
diff --git a/cayenne/src/test/java/org/apache/cayenne/query/MappedQueryIT.java b/cayenne/src/test/java/org/apache/cayenne/query/MappedQueryIT.java
index 475ad1981..a1215e638 100644
--- a/cayenne/src/test/java/org/apache/cayenne/query/MappedQueryIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/query/MappedQueryIT.java
@@ -19,7 +19,7 @@
 package org.apache.cayenne.query;
 
 import org.apache.cayenne.DataRow;
-import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.QueryResultItem;
 import org.apache.cayenne.ResultBatchIterator;
 import org.apache.cayenne.log.NoopSQLLogger;
 import org.apache.cayenne.test.jdbc.TableHelper;
@@ -127,9 +127,10 @@ public class MappedQueryIT {
 
         env.context().commitChanges();
 
-        List<?> artists = runProcedureSelect(MappedSelect.query("ProcedureQuery", Artist.class)
+        List<QueryResultItem> result = runProcedureSelect(MappedSelect.query("ProcedureQuery", Artist.class)
                 .param("aName", "An Artist")
-                .param("paintingPrice", 3000).forceNoCache()).firstList();
+                .param("paintingPrice", 3000).forceNoCache());
+        List<?> artists = ((QueryResultItem.Select<?>) result.getFirst()).objects();
 
         // check the results
         assertNotNull(artists, "Null result from StoredProcedure.");
@@ -165,7 +166,7 @@ public class MappedQueryIT {
                 query3.getMetaData(env.context().getEntityResolver()).getCacheKey());
     }
 
-    protected QueryResponse runProcedureSelect(AbstractMappedQuery q) throws Exception {
+    protected List<QueryResultItem> runProcedureSelect(AbstractMappedQuery q) throws Exception {
         // Sybase blows whenever a transaction wraps a SP, so turn off
         // transactions
 
diff --git a/cayenne/src/test/java/org/apache/cayenne/util/GenericResponseTest.java b/cayenne/src/test/java/org/apache/cayenne/util/GenericResponseTest.java
deleted file mode 100644
index 8a02c37bb..000000000
--- a/cayenne/src/test/java/org/apache/cayenne/util/GenericResponseTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*****************************************************************
- *   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
- *
- *    https://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.cayenne.util;
-
-import org.junit.jupiter.api.Test;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class GenericResponseTest {
-
-    @Test
-    public void creation() {
-        List list = new ArrayList();
-        list.add(new HashMap());
-
-        GenericResponse r = new GenericResponse();
-        r.addBatchUpdateCount(new int[] { 1, 2, 3 });
-        r.addResultList(list);
-
-        assertEquals(2, r.size());
-
-        assertTrue(r.next());
-        assertFalse(r.isList());
-
-        int[] srInt = r.currentUpdateCount();
-        assertEquals(3, srInt.length);
-        assertEquals(2, srInt[1]);
-
-        assertTrue(r.next());
-        assertTrue(r.isList());
-
-        assertEquals(list, r.currentList());
-
-        assertFalse(r.next());
-    }
-
-    @Test
-    public void next() {
-        List<Integer> result = List.of(1, 2, 3);
-        GenericResponse r = new GenericResponse();
-        r.addResultList(result);
-
-        assertTrue(r.next());
-        assertTrue(r.isList());
-        assertEquals(result, r.currentList());
-        assertFalse(r.next());
-    }
-
-}
diff --git a/cayenne/src/test/java/org/apache/cayenne/util/ListResponseTest.java b/cayenne/src/test/java/org/apache/cayenne/util/ListResponseTest.java
deleted file mode 100644
index 6fb0c9927..000000000
--- a/cayenne/src/test/java/org/apache/cayenne/util/ListResponseTest.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*****************************************************************
- *   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
- *
- *    https://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.cayenne.util;
-
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class ListResponseTest {
-
-    @Test
-    public void creation() {
-
-        Object object = new Object();
-        ListResponse r = new ListResponse(object);
-
-        assertEquals(1, r.size());
-
-        assertTrue(r.next());
-        assertTrue(r.isList());
-
-        List currentList = r.currentList();
-        assertEquals(1, currentList.size());
-        assertTrue(currentList.contains(object));
-
-        assertFalse(r.next());
-
-        r.reset();
-        assertTrue(r.next());
-
-        assertSame(currentList, r.firstList());
-    }
-
-    @Test
-    public void next() {
-        List<Integer> result = List.of(1, 2, 3);
-        ListResponse r = new ListResponse(result);
-
-        assertTrue(r.next());
-        assertTrue(r.isList());
-        assertEquals(result, r.currentList());
-        assertFalse(r.next());
-    }
-
-}
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/objectContext.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/objectContext.adoc
index 8aa9eeb49..02ccb2b41 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/objectContext.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/objectContext.adoc
@@ -84,7 +84,15 @@ While not commonly used, it is nevertheless important in some situations. E.g.:
 
 [source, java]
 ----
-QueryResponse response = context.performGenericQuery(query);
+List<QueryResultItem> result = context.performGenericQuery(query);
+for (QueryResultItem item : result) {
+    switch (item) {
+        case QueryResultItem.Select<?> select -> process(select.objects());
+        case QueryResultItem.Update update -> process(update.counts());
+        case QueryResultItem.Iterator<?> iterator -> process(iterator.iterator());
+        case QueryResultItem.OutParameters out -> process(out.values());
+    }
+}
 ----
 
 When multiple such queries need to run together, execute them inside an explicit transaction: