Repository: datastax/cassandra
Pull request #1638: CNDB-13353: Queries with index-based ordering should reject aggregation queries
Author: login: adelapena
Created: 2025-03-17T16:10:52Z | Merged: 2025-03-27T15:56:15Z

Queries with index-based ordering (ANN, BM25, generic `ORDER BY`) don't support CQL's aggregation queries, such as `GROUP BY` or `SELECT COUNT(*)`. The reason is that the coordinator internally uses paging to fetch more results from the replicas until the aggregated limit is satisfied. Since top-k index-based ordering doesn't support paging, these queries will fail. 

We should reject these queries until we have a way to support them.



Diff:
diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
index 8919f9010f58..f850a35ca2d5 100644
--- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
@@ -114,6 +114,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
 
     private static final Logger logger = LoggerFactory.getLogger(SelectStatement.class);
     private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(SelectStatement.logger, 1, TimeUnit.MINUTES);
+    public static final String TOPK_AGGREGATION_ERROR = "Top-K queries can not be run with aggregation";
     public static final String TOPK_CONSISTENCY_LEVEL_ERROR = "Top-K queries can only be run with consistency level ONE/LOCAL_ONE. Consistency level %s was used.";
     public static final String TOPK_CONSISTENCY_LEVEL_WARNING = "Top-K queries can only be run with consistency level ONE " +
                                                                 "/ LOCAL_ONE / NODE_LOCAL. Consistency level %s was requested. " +
@@ -431,10 +432,16 @@ public ReadQuery getQuery(QueryState queryState,
 
             // We don't support offset for top-k queries.
             checkFalse(userOffset != NO_OFFSET, String.format(TOPK_OFFSET_ERROR, userOffset));
+
+            // We don't support aggregation for top-k queries because we don't support paging.
+            checkFalse(aggregationSpec != null, TOPK_AGGREGATION_ERROR);
         }
 
         selectOptions.validate(queryState, table.keyspace, userLimit);
 
+        // If there's a secondary index that the command can use, have it validate the request parameters.
+        query.maybeValidateIndexes();
+
         return query;
     }
 
@@ -783,13 +790,7 @@ private ReadQuery getRangeCommand(QueryOptions options, ColumnFilter columnFilte
         if (keyBounds == null)
             return ReadQuery.empty(table);
 
-        ReadQuery command =
-            PartitionRangeReadQuery.create(table, nowInSec, columnFilter, rowFilter, limit, new DataRange(keyBounds, clusteringIndexFilter));
-
-        // If there's a secondary index that the command can use, have it validate the request parameters.
-        command.maybeValidateIndexes();
-
-        return command;
+        return PartitionRangeReadQuery.create(table, nowInSec, columnFilter, rowFilter, limit, new DataRange(keyBounds, clusteringIndexFilter));
     }
 
     private ClusteringIndexFilter makeClusteringIndexFilter(QueryOptions options, ColumnFilter columnFilter, QueryState queryState)
diff --git a/test/unit/org/apache/cassandra/index/sai/cql/BM25Test.java b/test/unit/org/apache/cassandra/index/sai/cql/BM25Test.java
index facf8c52b579..b0f1b84369ee 100644
--- a/test/unit/org/apache/cassandra/index/sai/cql/BM25Test.java
+++ b/test/unit/org/apache/cassandra/index/sai/cql/BM25Test.java
@@ -27,6 +27,8 @@
 import org.junit.Test;
 
 import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.cql3.statements.SelectStatement;
+import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.index.sai.SAITester;
 import org.apache.cassandra.index.sai.SAIUtil;
 import org.apache.cassandra.index.sai.disk.format.Version;
@@ -34,6 +36,7 @@
 import org.apache.cassandra.index.sai.plan.QueryController;
 
 import static org.apache.cassandra.index.sai.analyzer.AnalyzerEqOperatorSupport.EQ_AMBIGUOUS_ERROR;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
 import static org.junit.Assert.assertEquals;
 
@@ -557,8 +560,32 @@ public void testWildcardSelection()
 
         var result = execute("SELECT * FROM %s ORDER BY v BM25 OF 'apple' LIMIT 3");
         assertThat(result).hasSize(1);
+    }
+
+    @Test
+    public void cannotHaveAggregationOnBM25Query()
+    {
+        createSimpleTable();
+
+        execute("INSERT INTO %s (k, v) VALUES (1, '4')");
+        execute("INSERT INTO %s (k, v) VALUES (2, '3')");
+        execute("INSERT INTO %s (k, v) VALUES (3, '2')");
+        execute("INSERT INTO %s (k, v) VALUES (4, '1')");
+
+        assertThatThrownBy(() -> execute("SELECT max(v) FROM %s ORDER BY v BM25 OF 'apple' LIMIT 4"))
+                .isInstanceOf(InvalidRequestException.class)
+                .hasMessage(SelectStatement.TOPK_AGGREGATION_ERROR);
+
+        assertThatThrownBy(() -> execute("SELECT max(v) FROM %s WHERE k = 1 ORDER BY v BM25 OF 'apple' LIMIT 4"))
+                .isInstanceOf(InvalidRequestException.class)
+                .hasMessage(SelectStatement.TOPK_AGGREGATION_ERROR);
+
+        assertThatThrownBy(() -> execute("SELECT * FROM %s GROUP BY k ORDER BY v BM25 OF 'apple' LIMIT 4"))
+                .isInstanceOf(InvalidRequestException.class)
+                .hasMessage(SelectStatement.TOPK_AGGREGATION_ERROR);
 
-        var result2 = execute("SELECT * FROM %s GROUP BY k, c ORDER BY v BM25 OF 'apple' LIMIT 3");
-        assertThat(result2).hasSize(1);
+        assertThatThrownBy(() -> execute("SELECT count(*) FROM %s ORDER BY v BM25 OF 'apple' LIMIT 4"))
+                .isInstanceOf(InvalidRequestException.class)
+                .hasMessage(SelectStatement.TOPK_AGGREGATION_ERROR);
     }
 }
diff --git a/test/unit/org/apache/cassandra/index/sai/cql/GenericOrderByTest.java b/test/unit/org/apache/cassandra/index/sai/cql/GenericOrderByTest.java
index f132e163d8a1..4bf43bf1571a 100644
--- a/test/unit/org/apache/cassandra/index/sai/cql/GenericOrderByTest.java
+++ b/test/unit/org/apache/cassandra/index/sai/cql/GenericOrderByTest.java
@@ -23,9 +23,13 @@
 import org.junit.Test;
 
 import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.statements.SelectStatement;
+import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.index.sai.SAITester;
 import org.apache.cassandra.index.sai.plan.QueryController;
 
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
 public class GenericOrderByTest extends SAITester
 {
     @Test
@@ -226,4 +230,31 @@ public void testSelectionAndOrderByOnTheSameColumnWithLargeRowCountDesc() throws
         testSelectionAndOrderByOnTheSameColumnWithLargeRowCount(false);
     }
 
+    @Test
+    public void cannotHaveAggregationOnOrderByQuery()
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v int)");
+        createIndex("CREATE CUSTOM INDEX ON %s(v) USING 'StorageAttachedIndex'");
+
+        execute("INSERT INTO %s (k, v) VALUES (1, 4)");
+        execute("INSERT INTO %s (k, v) VALUES (2, 3)");
+        execute("INSERT INTO %s (k, v) VALUES (3, 2)");
+        execute("INSERT INTO %s (k, v) VALUES (4, 1)");
+
+        assertThatThrownBy(() -> execute("SELECT sum(v) FROM %s ORDER BY v LIMIT 4"))
+                .isInstanceOf(InvalidRequestException.class)
+                .hasMessage(SelectStatement.TOPK_AGGREGATION_ERROR);
+
+        assertThatThrownBy(() -> execute("SELECT sum(v) FROM %s WHERE k = 1 ORDER BY v LIMIT 4"))
+                .isInstanceOf(InvalidRequestException.class)
+                .hasMessage(SelectStatement.TOPK_AGGREGATION_ERROR);
+
+        assertThatThrownBy(() -> execute("SELECT * FROM %s GROUP BY k ORDER BY v LIMIT 4"))
+                .isInstanceOf(InvalidRequestException.class)
+                .hasMessage(SelectStatement.TOPK_AGGREGATION_ERROR);
+
+        assertThatThrownBy(() -> execute("SELECT count(*) FROM %s ORDER BY v LIMIT 4"))
+                .isInstanceOf(InvalidRequestException.class)
+                .hasMessage(SelectStatement.TOPK_AGGREGATION_ERROR);
+    }
 }
diff --git a/test/unit/org/apache/cassandra/index/sai/cql/VectorInvalidQueryTest.java b/test/unit/org/apache/cassandra/index/sai/cql/VectorInvalidQueryTest.java
index 30b69856915d..5a827499ab1a 100644
--- a/test/unit/org/apache/cassandra/index/sai/cql/VectorInvalidQueryTest.java
+++ b/test/unit/org/apache/cassandra/index/sai/cql/VectorInvalidQueryTest.java
@@ -240,6 +240,34 @@ public void disallowClusteringColumnPredicateWithoutSupportingIndex()
         assertRows(execute("SELECT num FROM %s WHERE pk=3 AND num > 3 ORDER BY v ANN OF [1,1] LIMIT 1"), row(4));
     }
 
+    @Test
+    public void cannotHaveAggregationOnANNQuery()
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v vector<float, 1>, c int)");
+        createIndex("CREATE CUSTOM INDEX ON %s(v) USING 'StorageAttachedIndex' WITH OPTIONS = {'similarity_function' : 'euclidean'}");
+
+        execute("INSERT INTO %s (k, v, c) VALUES (1, [4], 1)");
+        execute("INSERT INTO %s (k, v, c) VALUES (2, [3], 10)");
+        execute("INSERT INTO %s (k, v, c) VALUES (3, [2], 100)");
+        execute("INSERT INTO %s (k, v, c) VALUES (4, [1], 1000)");
+
+        assertThatThrownBy(() -> execute("SELECT sum(c) FROM %s ORDER BY v ANN OF [0] LIMIT 4"))
+                .isInstanceOf(InvalidRequestException.class)
+                .hasMessage(SelectStatement.TOPK_AGGREGATION_ERROR);
+
+        assertThatThrownBy(() -> execute("SELECT sum(c) FROM %s WHERE k = 1 ORDER BY v ANN OF [0] LIMIT 4"))
+                .isInstanceOf(InvalidRequestException.class)
+                .hasMessage(SelectStatement.TOPK_AGGREGATION_ERROR);
+
+        assertThatThrownBy(() -> execute("SELECT * FROM %s GROUP BY k ORDER BY v ANN OF [0] LIMIT 4"))
+                .isInstanceOf(InvalidRequestException.class)
+                .hasMessage(SelectStatement.TOPK_AGGREGATION_ERROR);
+
+        assertThatThrownBy(() -> execute("SELECT count(*) FROM %s ORDER BY v ANN OF [0] LIMIT 4"))
+                .isInstanceOf(InvalidRequestException.class)
+                .hasMessage(SelectStatement.TOPK_AGGREGATION_ERROR);
+    }
+
     @Test
     public void canOnlyExecuteWithCorrectConsistencyLevel()
     {


Reviews:
user: login: eolivelli
body: +1
state: APPROVED
author_association: NONE
submitted_at: 2025-03-25T18:47:36Z
user: login: ekaterinadimitrova2
body: +1, just one unused import that can be removed on commit. Thanks. 
state: APPROVED
author_association: NONE
submitted_at: 2025-03-25T20:24:02Z
user: login: adelapena
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T12:54:42Z
user: login: ekaterinadimitrova2
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T12:58:49Z
user: login: adelapena
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T13:54:40Z
user: login: ekaterinadimitrova2
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T19:36:23Z
user: login: adelapena
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T19:59:29Z
user: login: ekaterinadimitrova2
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T20:08:39Z
user: login: adelapena
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T20:20:00Z
user: login: adelapena
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T11:34:02Z
user: login: ekaterinadimitrova2
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T14:10:46Z
user: login: adelapena
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T14:58:34Z
user: login: ekaterinadimitrova2
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T15:23:38Z

Review comments:
diff_hunk: @@ -20,6 +20,7 @@
 
 import java.util.Collections;
 
+import com.datastax.driver.core.exceptions.InvalidQueryException;
path: test/unit/org/apache/cassandra/index/sai/cql/VectorInvalidQueryTest.java
user: login: ekaterinadimitrova2
body: Not needed, we check for `InvalidRequestException`
created_at: 2025-03-25T20:17:29Z
author_association: NONE
diff_hunk: @@ -786,6 +787,11 @@ private ReadQuery getRangeCommand(QueryOptions options, ColumnFilter columnFilte
         ReadQuery command =
             PartitionRangeReadQuery.create(table, nowInSec, columnFilter, rowFilter, limit, new DataRange(keyBounds, clusteringIndexFilter));
 
+        if (command.isTopK())
+        {
+            checkFalse(aggregationSpec != null, TOPK_AGGREGATION_ERROR);
path: src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
user: login: ekaterinadimitrova2
body: So in Apache, the check being only in the `execute` method, there is a bug? I haven't tested, but this one here covers also `executeInternal `and `executeLocally` and in Apache the check is in `execute`, so that made me wonder. :-) 
created_at: 2025-03-25T20:17:48Z
author_association: NONE
diff_hunk: @@ -20,6 +20,7 @@
 
 import java.util.Collections;
 
+import com.datastax.driver.core.exceptions.InvalidQueryException;
path: test/unit/org/apache/cassandra/index/sai/cql/VectorInvalidQueryTest.java
user: login: adelapena
body: Removed.
created_at: 2025-03-26T12:54:42Z
author_association: NONE
diff_hunk: @@ -575,19 +572,19 @@ public void cannotHaveAggregationOnBM25Query()
         execute("INSERT INTO %s (k, v) VALUES (3, '2')");
         execute("INSERT INTO %s (k, v) VALUES (4, '1')");
 
-        assertThatThrownBy(() -> execute("SELECT max(v) FROM %s ORDER BY v LIMIT 4"))
+        assertThatThrownBy(() -> execute("SELECT max(v) FROM %s ORDER BY v BM25 OF 'apple' LIMIT 4"))
path: test/unit/org/apache/cassandra/index/sai/cql/BM25Test.java
user: login: ekaterinadimitrova2
body: Sorry, I missed that, good you saw it
created_at: 2025-03-26T12:58:49Z
author_association: NONE
diff_hunk: @@ -786,6 +787,11 @@ private ReadQuery getRangeCommand(QueryOptions options, ColumnFilter columnFilte
         ReadQuery command =
             PartitionRangeReadQuery.create(table, nowInSec, columnFilter, rowFilter, limit, new DataRange(keyBounds, clusteringIndexFilter));
 
+        if (command.isTopK())
+        {
+            checkFalse(aggregationSpec != null, TOPK_AGGREGATION_ERROR);
path: src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
user: login: adelapena
body: This is a very good point. I think I did that change while adding the query offset ([CNDB-9541](https://github.com/datastax/cassandra/commit/c84116c9bf5786fe2574e922dcbf7343dbbe10ae#diff-2ffcbaa5b5edfee0453c64897504f4e3d88150de8977e72d604c4bb688cb3d9b)), but I don't remember why. I think it might be because I found validation missing when calling `CQLTester#execute`, due to the problems addressed by [CNDB-12905](https://github.com/riptano/cndb/issues/12905). 

I have reverted that part and updated `SelectOffsetTest` to use coordinator execution. Skipping validation on replicas and internal queries should give us some (tiny) performance gains. Let's see if something else breaks on CI.
created_at: 2025-03-26T13:54:40Z
author_association: NONE
diff_hunk: @@ -362,8 +363,34 @@ public ResultMessage.Rows execute(QueryState queryState, QueryOptions options, l
         PageSize pageSize = options.getPageSize();
 
         Selectors selectors = selection.newSelectors(options);
+
         ReadQuery query = getQuery(queryState, options, selectors.getColumnFilter(), nowInSec, userLimit, userPerPartitionLimit, userOffset);
 
+        // Handle additional validation for topK queries
+        if (query.isTopK())
+        {
+            // We aren't going to allow SERIAL at all, so we can error out on those.
+            checkFalse(options.getConsistency() == ConsistencyLevel.LOCAL_SERIAL ||
+                       options.getConsistency() == ConsistencyLevel.SERIAL,
+                    String.format(TOPK_CONSISTENCY_LEVEL_ERROR, options.getConsistency()));
path: src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
user: login: ekaterinadimitrova2
body: formatting
created_at: 2025-03-26T19:36:23Z
author_association: NONE
diff_hunk: @@ -362,8 +363,34 @@ public ResultMessage.Rows execute(QueryState queryState, QueryOptions options, l
         PageSize pageSize = options.getPageSize();
 
         Selectors selectors = selection.newSelectors(options);
+
         ReadQuery query = getQuery(queryState, options, selectors.getColumnFilter(), nowInSec, userLimit, userPerPartitionLimit, userOffset);
 
+        // Handle additional validation for topK queries
+        if (query.isTopK())
+        {
+            // We aren't going to allow SERIAL at all, so we can error out on those.
+            checkFalse(options.getConsistency() == ConsistencyLevel.LOCAL_SERIAL ||
+                       options.getConsistency() == ConsistencyLevel.SERIAL,
+                    String.format(TOPK_CONSISTENCY_LEVEL_ERROR, options.getConsistency()));
path: src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
user: login: adelapena
body: Fixed.
created_at: 2025-03-26T19:59:29Z
author_association: NONE
diff_hunk: @@ -786,6 +787,11 @@ private ReadQuery getRangeCommand(QueryOptions options, ColumnFilter columnFilte
         ReadQuery command =
             PartitionRangeReadQuery.create(table, nowInSec, columnFilter, rowFilter, limit, new DataRange(keyBounds, clusteringIndexFilter));
 
+        if (command.isTopK())
+        {
+            checkFalse(aggregationSpec != null, TOPK_AGGREGATION_ERROR);
path: src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
user: login: ekaterinadimitrova2
body: > but I don't remember why

I was not involved in CNDB-9541, so I am not sure either. If needed, I can try to look into that rather big patch.
Otherwise, the current change makes sense to me. I hope I do not miss anything from the other patch. 
created_at: 2025-03-26T20:08:39Z
author_association: NONE
diff_hunk: @@ -786,6 +787,11 @@ private ReadQuery getRangeCommand(QueryOptions options, ColumnFilter columnFilte
         ReadQuery command =
             PartitionRangeReadQuery.create(table, nowInSec, columnFilter, rowFilter, limit, new DataRange(keyBounds, clusteringIndexFilter));
 
+        if (command.isTopK())
+        {
+            checkFalse(aggregationSpec != null, TOPK_AGGREGATION_ERROR);
path: src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
user: login: adelapena
body: I’m 83% sure my past self didn’t realize the issue with `CQLTester#execute` and wrongly thought that all versions of `SELECT` execute should be validated. So it’d be great if you can quickly skim through that patch but I think we are good.
created_at: 2025-03-26T20:19:59Z
author_association: NONE
diff_hunk: @@ -786,6 +787,11 @@ private ReadQuery getRangeCommand(QueryOptions options, ColumnFilter columnFilte
         ReadQuery command =
             PartitionRangeReadQuery.create(table, nowInSec, columnFilter, rowFilter, limit, new DataRange(keyBounds, clusteringIndexFilter));
 
+        if (command.isTopK())
+        {
+            checkFalse(aggregationSpec != null, TOPK_AGGREGATION_ERROR);
path: src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
user: login: adelapena
body: Actually, I think we can safely move the rest of the validations in `SelectStatement#getRangeCommand` to `#execute` and save us a bunch of replica-side validations. I've pushed an additional commit doing this.
created_at: 2025-03-27T11:34:02Z
author_association: NONE
diff_hunk: @@ -362,8 +363,40 @@ public ResultMessage.Rows execute(QueryState queryState, QueryOptions options, l
         PageSize pageSize = options.getPageSize();
 
         Selectors selectors = selection.newSelectors(options);
+
         ReadQuery query = getQuery(queryState, options, selectors.getColumnFilter(), nowInSec, userLimit, userPerPartitionLimit, userOffset);
 
+        // Handle additional validation for topK queries
+        if (query.isTopK())
+        {
+            // We aren't going to allow SERIAL at all, so we can error out on those.
+            checkFalse(options.getConsistency() == ConsistencyLevel.LOCAL_SERIAL ||
+                       options.getConsistency() == ConsistencyLevel.SERIAL,
+                       String.format(TOPK_CONSISTENCY_LEVEL_ERROR, options.getConsistency()));
+
+            // Consistency levels with more than one replica are downgraded to ONE/LOCAL_ONE.
+            if (options.getConsistency() != ConsistencyLevel.ONE &&
+                options.getConsistency() != ConsistencyLevel.LOCAL_ONE &&
+                options.getConsistency() != ConsistencyLevel.NODE_LOCAL)
+            {
+                ConsistencyLevel supplied = options.getConsistency();
+                ConsistencyLevel downgrade = supplied.isDatacenterLocal() ? ConsistencyLevel.LOCAL_ONE : ConsistencyLevel.ONE;
+                options.updateConsistency(downgrade);
+                ClientWarn.instance.warn(String.format(TOPK_CONSISTENCY_LEVEL_WARNING, supplied, downgrade));
+            }
+
+            // We don't support offset for top-k queries.
+            checkFalse(userOffset != NO_OFFSET, String.format(TOPK_OFFSET_ERROR, userOffset));
+
+            // We don't support aggregation for top-k queries because we don't support paging.
+            checkFalse(aggregationSpec != null, TOPK_AGGREGATION_ERROR);
+        }
+
+        selectOptions.validate(queryState, table.keyspace, userLimit);
+
+        // If there's a secondary index that the query can use, have it validate the request parameters.
+        query.maybeValidateIndexes();
path: src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
user: login: ekaterinadimitrova2
body: I think we should not move the index validation. Currently the queryPlan (we do not serialize it) is recreated on the replica and in case new index was created in the meantime, it may not have been validated on the coordinator. 
created_at: 2025-03-27T14:10:45Z
author_association: NONE
diff_hunk: @@ -362,8 +363,40 @@ public ResultMessage.Rows execute(QueryState queryState, QueryOptions options, l
         PageSize pageSize = options.getPageSize();
 
         Selectors selectors = selection.newSelectors(options);
+
         ReadQuery query = getQuery(queryState, options, selectors.getColumnFilter(), nowInSec, userLimit, userPerPartitionLimit, userOffset);
 
+        // Handle additional validation for topK queries
+        if (query.isTopK())
+        {
+            // We aren't going to allow SERIAL at all, so we can error out on those.
+            checkFalse(options.getConsistency() == ConsistencyLevel.LOCAL_SERIAL ||
+                       options.getConsistency() == ConsistencyLevel.SERIAL,
+                       String.format(TOPK_CONSISTENCY_LEVEL_ERROR, options.getConsistency()));
+
+            // Consistency levels with more than one replica are downgraded to ONE/LOCAL_ONE.
+            if (options.getConsistency() != ConsistencyLevel.ONE &&
+                options.getConsistency() != ConsistencyLevel.LOCAL_ONE &&
+                options.getConsistency() != ConsistencyLevel.NODE_LOCAL)
+            {
+                ConsistencyLevel supplied = options.getConsistency();
+                ConsistencyLevel downgrade = supplied.isDatacenterLocal() ? ConsistencyLevel.LOCAL_ONE : ConsistencyLevel.ONE;
+                options.updateConsistency(downgrade);
+                ClientWarn.instance.warn(String.format(TOPK_CONSISTENCY_LEVEL_WARNING, supplied, downgrade));
+            }
+
+            // We don't support offset for top-k queries.
+            checkFalse(userOffset != NO_OFFSET, String.format(TOPK_OFFSET_ERROR, userOffset));
+
+            // We don't support aggregation for top-k queries because we don't support paging.
+            checkFalse(aggregationSpec != null, TOPK_AGGREGATION_ERROR);
+        }
+
+        selectOptions.validate(queryState, table.keyspace, userLimit);
+
+        // If there's a secondary index that the query can use, have it validate the request parameters.
+        query.maybeValidateIndexes();
path: src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
user: login: adelapena
body: I think I got confused about `CQLStatement#executeLocally`. It's not run on the replicas, where CQL has already been parsed and we are deserializing a `ReadQuery`. So there won't be any kind of CQL validation, and the building of a new `Index.QueryPlan` won't trigger any of these validations either way.

I understand that `CQLStatement#executeLocally` is meant to be used on the coordinator for queries with consistency level `NODE_LOCAL`. So if we moved validation to `CQLStatement#execute` only those queries would miss validation, and we don't want that. So I'm leaving all validations in `CQLStatement#getQuery` so all versions of execute can use it. We might want to exclude `executeInternal` at some point, but I think we should do it separately.

Another issue is that I've found that dtests with `NODE_LOCAL` consistency level seem to end up using `ONE` internally, I don't know why. The same happens running locally and using cqlsh. There doesn't seem to be a lot of test coverage for that consistency level. So that's something to investigate separately.
created_at: 2025-03-27T14:58:33Z
author_association: NONE
diff_hunk: @@ -362,8 +363,40 @@ public ResultMessage.Rows execute(QueryState queryState, QueryOptions options, l
         PageSize pageSize = options.getPageSize();
 
         Selectors selectors = selection.newSelectors(options);
+
         ReadQuery query = getQuery(queryState, options, selectors.getColumnFilter(), nowInSec, userLimit, userPerPartitionLimit, userOffset);
 
+        // Handle additional validation for topK queries
+        if (query.isTopK())
+        {
+            // We aren't going to allow SERIAL at all, so we can error out on those.
+            checkFalse(options.getConsistency() == ConsistencyLevel.LOCAL_SERIAL ||
+                       options.getConsistency() == ConsistencyLevel.SERIAL,
+                       String.format(TOPK_CONSISTENCY_LEVEL_ERROR, options.getConsistency()));
+
+            // Consistency levels with more than one replica are downgraded to ONE/LOCAL_ONE.
+            if (options.getConsistency() != ConsistencyLevel.ONE &&
+                options.getConsistency() != ConsistencyLevel.LOCAL_ONE &&
+                options.getConsistency() != ConsistencyLevel.NODE_LOCAL)
+            {
+                ConsistencyLevel supplied = options.getConsistency();
+                ConsistencyLevel downgrade = supplied.isDatacenterLocal() ? ConsistencyLevel.LOCAL_ONE : ConsistencyLevel.ONE;
+                options.updateConsistency(downgrade);
+                ClientWarn.instance.warn(String.format(TOPK_CONSISTENCY_LEVEL_WARNING, supplied, downgrade));
+            }
+
+            // We don't support offset for top-k queries.
+            checkFalse(userOffset != NO_OFFSET, String.format(TOPK_OFFSET_ERROR, userOffset));
+
+            // We don't support aggregation for top-k queries because we don't support paging.
+            checkFalse(aggregationSpec != null, TOPK_AGGREGATION_ERROR);
+        }
+
+        selectOptions.validate(queryState, table.keyspace, userLimit);
+
+        // If there's a secondary index that the query can use, have it validate the request parameters.
+        query.maybeValidateIndexes();
path: src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
user: login: ekaterinadimitrova2
body: I actually tested locally but with a different consistency level, so I did not catch that. That's a good point. Let's open a follow-up tickets. For this ticket, we are good. Thanks!
created_at: 2025-03-27T15:23:37Z
author_association: NONE

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-17T16:11:10Z
body: ### Checklist before you submit for review
- [ ] Make sure there is a PR in the CNDB project updating the Converged Cassandra version
- [ ] Use `NoSpamLogger` for log lines that may appear frequently in the logs
- [ ] Verify test results on Butler
- [ ] Test coverage for new/modified code is > 80%
- [ ] Proper code formatting
- [ ] Proper title for each commit staring with the project-issue number, like CNDB-1234
- [ ] Each commit has a meaningful description
- [ ] Each commit is not very long and contains related changes
- [ ] Renames, moves and reformatting are in distinct commits

author_association: NONE
user: login: adelapena
created_at: 2025-03-26T12:53:55Z
body: Thanks for the reviews. 

I have just rebased, updated `BM25Test#cannotHaveAggregationOnBM25Query` to actually use BM25, and removed the `GROUP BY` check from `BM25Test#testWildcardSelection` (it's covered by `BM25Test#cannotHaveAggregationOnBM25Query`).

The CNDB PR is https://github.com/riptano/cndb/pull/13460.
author_association: NONE
user: login: ekaterinadimitrova2
created_at: 2025-03-26T13:07:24Z
body: Still +1 on clean CI
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-03-27T15:27:16Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=cassandra-stargazer&pullRequest=1638) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=cassandra-stargazer&pullRequest=1638&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=cassandra-stargazer&pullRequest=1638&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=cassandra-stargazer&pullRequest=1638&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=cassandra-stargazer&pullRequest=1638&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=cassandra-stargazer&pullRequest=1638&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=cassandra-stargazer&pullRequest=1638)


author_association: NONE
user: login: cassci-bot
created_at: 2025-03-27T15:31:50Z
body: <!-- ds-cassandra-build-status -->
### :heavy_check_mark: Build ds-cassandra-pr-gate/PR-1638 approved by Butler

---
Approved by Butler
See build details [here](http://butler-stargazer.aws.dsinternal.org/#/ci/upstream/compare/ds-cassandra-pr-gate/CNDB-13353-main/to/ds-cassandra-build-nightly/main)

author_association: NONE
user: login: eolivelli
created_at: 2025-03-27T15:56:27Z
body: CNDB CI looks good https://github.com/riptano/cndb/pull/13460#pullrequestreview-2722313670

I have merged this patch, thanks
author_association: NONE