This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch cassandra-6.0
in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/cassandra-6.0 by this push:
new 6668e08542 Support wildcard * in excluded_indexes index hints
6668e08542 is described below
commit 6668e08542ff4252ea0fd95f092856d8cda730d8
Author: Galarcel <[email protected]>
AuthorDate: Mon Oct 12 13:19:24 2026 -0700
Support wildcard * in excluded_indexes index hints
Extends the index hints feature from CASSANDRA-18112 to accept a '*'
wildcard in excluded_indexes, so a query can exclude all applicable
indexes without enumerating them by name:
SELECT * FROM t WHERE v = 0 ALLOW FILTERING
WITH excluded_indexes = {*};
The wildcard is resolved by the coordinator at validation time in
IndexHints.fromCQLNames, expanded to the full concrete set of indexes
currently registered for the table. No messaging version bump is
required since replicas receive an already-resolved set of excluded
indexes.
'*' is rejected in included_indexes (WILDCARD_INCLUDED_INDEXES_ERROR)
since 'include all indexes' has no defined semantics.
patch by Mauricio Martinez; reviewed by Andrés de la Peña and Caleb
Rackliffe for CASSANDRA-21715
---
CHANGES.txt | 1 +
pylib/cqlshlib/cql3handling.py | 4 +-
src/antlr/Parser.g | 11 ++-
.../org/apache/cassandra/cql3/QualifiedName.java | 15 ++++
.../org/apache/cassandra/db/filter/IndexHints.java | 52 +++++++++++--
.../org/apache/cassandra/db/filter/IndexHints.md | 22 ++++++
.../test/sai/IndexHintsDistributedTest.java | 1 +
.../apache/cassandra/db/filter/IndexHintsTest.java | 90 ++++++++++++++++++++++
8 files changed, 185 insertions(+), 11 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 02b96a4401..b1ef9963c8 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
6.0-alpha3
+ * Support wildcard * in excluded_indexes index hints (CASSANDRA-21715)
* Prepare scalar constraints when column metadata is created so they are
enforced after deserialization (CASSANDRA-21818)
* Throw InvalidConstraintDefinitionException for malformed LENGTH and
OCTET_LENGTH constraint terms (CASSANDRA-21817)
* Support byte budgets for client query pages and internal replica reads,
including aggregation subpages (CASSANDRA-11745)
diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py
index a83c6535b8..592c4ca75b 100644
--- a/pylib/cqlshlib/cql3handling.py
+++ b/pylib/cqlshlib/cql3handling.py
@@ -820,10 +820,12 @@ syntax_rules += r'''
<identifiers> ::= "{" <identifier> ( "," <identifier> )* "}"
;
+<identifiersOrStars> ::= "{" ( <identifier> | <star> ) ( "," ( <identifier> |
<star> ) )* "}"
+ ;
<options> ::= <option> ( "AND" <option> )*
;
<option> ::= "included_indexes" "=" <identifiers>
- | "excluded_indexes" "=" <identifiers>
+ | "excluded_indexes" "=" <identifiersOrStars>
;
<aggregateMathFunctions> ::= "COUNT" "(" star=( "*" | "1" ) ")"
diff --git a/src/antlr/Parser.g b/src/antlr/Parser.g
index e2bdf3af6b..03517414b8 100644
--- a/src/antlr/Parser.g
+++ b/src/antlr/Parser.g
@@ -1880,9 +1880,14 @@ indexName returns [QualifiedName name]
: (ksName[name] '.')? idxName[name]
;
-indexNames returns [Set<QualifiedName> names]
+indexNameOrWildcard returns [QualifiedName name]
+ : n=indexName { $name = n; }
+ | '\*' { $name = QualifiedName.WILDCARD; }
+ ;
+
+indexNamesOrWildcards returns [Set<QualifiedName> names]
@init { $names = new HashSet<QualifiedName>(); }
- : '{' ( t1=indexName { names.add(t1); } ( ',' tn=indexName {
names.add(tn); } )* )? '}'
+ : '{' ( t1=indexNameOrWildcard { names.add(t1); } ( ','
tn=indexNameOrWildcard { names.add(tn); } )* )? '}'
;
columnFamilyName returns [QualifiedName name]
@@ -2197,7 +2202,7 @@ indexProperty returns [String s]
property[PropertyDefinitions props]
: k=noncol_ident '=' simple=propertyValue { try {
$props.addProperty(k.toString(), simple); } catch (SyntaxException e) {
addRecognitionError(e.getMessage()); } }
| k=noncol_ident '=' map=fullMapLiteral { try {
$props.addProperty(k.toString(), convertPropertyMap(map)); } catch
(SyntaxException e) { addRecognitionError(e.getMessage()); } }
- | s=indexProperty '=' names=indexNames { try { $props.addProperty(s,
names); } catch (SyntaxException e) { addRecognitionError(e.getMessage()); } }
+ | s=indexProperty '=' names=indexNamesOrWildcards { try {
$props.addProperty(s, names); } catch (SyntaxException e) {
addRecognitionError(e.getMessage()); } }
;
propertyValue returns [String str]
diff --git a/src/java/org/apache/cassandra/cql3/QualifiedName.java
b/src/java/org/apache/cassandra/cql3/QualifiedName.java
index 51413d4df2..a9058fdf0e 100644
--- a/src/java/org/apache/cassandra/cql3/QualifiedName.java
+++ b/src/java/org/apache/cassandra/cql3/QualifiedName.java
@@ -26,6 +26,13 @@ import static
org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized;
*/
public class QualifiedName
{
+ /**
+ * A sentinel {@link QualifiedName} used to represent the {@code *}
wildcard in index hints
+ * (e.g. {@code WITH excluded_indexes = {*}}). It has no keyspace and uses
{@code "*"} as the
+ * name, which is not a valid identifier and can therefore never collide
with an actual index name.
+ */
+ public static final QualifiedName WILDCARD = new QualifiedName(null, "*");
+
/**
* The keyspace name as stored internally.
*/
@@ -67,6 +74,14 @@ public class QualifiedName
return keyspace;
}
+ /**
+ * @return {@code true} if this name represents the {@code *} wildcard,
{@code false} otherwise
+ */
+ public boolean isWildcard()
+ {
+ return !hasKeyspace() && "*".equals(name);
+ }
+
public void setName(String cf, boolean keepCase)
{
name = toInternalName(cf, keepCase);
diff --git a/src/java/org/apache/cassandra/db/filter/IndexHints.java
b/src/java/org/apache/cassandra/db/filter/IndexHints.java
index 2158a36aef..7df8d5ac41 100644
--- a/src/java/org/apache/cassandra/db/filter/IndexHints.java
+++ b/src/java/org/apache/cassandra/db/filter/IndexHints.java
@@ -61,6 +61,7 @@ public class IndexHints
public static final String MISSING_INDEX_ERROR = "Table %s doesn't have an
index named %s";
public static final String NON_INCLUDABLE_INDEXES_ERROR = "It's not
possible to use all the specified included indexes with this query.";
public static final String TOO_MANY_INDEXES_ERROR = "Cannot have more than
'secondary_indexes_per_table_fail_threshold' included/excluded indexes, found ";
+ public static final String WILDCARD_INCLUDED_INDEXES_ERROR = "The wildcard
'*' is not allowed in included_indexes.";
public static final IndexHints NONE = new
IndexHints(Collections.emptySet(), Collections.emptySet())
{
@@ -363,13 +364,20 @@ public class IndexHints
* </p>
* All the mentioned indexes should exist in the index registry of the
queried table,
* or an {@link InvalidRequestException} will be thrown.
+ * </p>
+ * The {@code excluded} set can contain the {@code *} wildcard,
represented by {@link QualifiedName#WILDCARD},
+ * which is expanded to all the indexes currently registered for the
table. The wildcard is not allowed in the
+ * {@code included} set, since there is no defined use case for "include
all indexes", and doing so will throw
+ * an {@link InvalidRequestException}.
*
* @param included the names of the indexes to include when executing the
query
- * @param excluded the names of the indexes to exclude when executing the
query
+ * @param excluded the names of the indexes to exclude when executing the
query, possibly containing the {@code *}
+ * wildcard to exclude all the indexes currently applicable to the table
* @param table the queried table
* @param indexRegistry the index registry of the queried table
* @return the index hints represented by the specified sets of CQL names
- * @throws InvalidRequestException if any of the specified indexes do not
exist in the specified index registry
+ * @throws InvalidRequestException if any of the specified indexes do not
exist in the specified index registry,
+ * or if the {@code *} wildcard is used in the {@code included} set
*/
public static IndexHints fromCQLNames(Set<QualifiedName> included,
Set<QualifiedName> excluded,
@@ -382,8 +390,18 @@ public class IndexHints
if (excluded != null && excluded.size() >
maxIncludedOrExcludedIndexCount())
throw new InvalidRequestException(TOO_MANY_INDEXES_ERROR +
excluded.size());
- IndexHints hints = IndexHints.create(fetchIndexes(included, table,
indexRegistry),
- fetchIndexes(excluded, table,
indexRegistry));
+ // The '*' wildcard is only supported for excluded_indexes: it's
expanded here to the full set of indexes
+ // currently registered for the table. This doesn't require explicit
protocol support because replicas
+ // receive an already-resolved, concrete set of excluded indexes.
+ Set<IndexMetadata> includedIndexes = fetchIndexes(included, table,
indexRegistry, false);
+ Set<IndexMetadata> excludedIndexes = fetchIndexes(excluded, table,
indexRegistry, true);
+
+ // The wildcard expansion above can produce a larger set than what the
user typed, so the size limit
+ // needs to be re-checked after expansion. The check above on the raw
CQL names wouldn't catch this.
+ if (excludedIndexes.size() > maxIncludedOrExcludedIndexCount())
+ throw new InvalidRequestException(TOO_MANY_INDEXES_ERROR +
excludedIndexes.size());
+
+ IndexHints hints = IndexHints.create(includedIndexes, excludedIndexes);
if (hints == IndexHints.NONE)
return hints;
@@ -414,17 +432,37 @@ public class IndexHints
return guardrail > 0 ? guardrail : 128;
}
- private static Set<IndexMetadata> fetchIndexes(Set<QualifiedName>
indexNames, TableMetadata table, IndexRegistry indexRegistry)
+ private static Set<IndexMetadata> fetchIndexes(Set<QualifiedName>
indexNames,
+ TableMetadata table,
+ IndexRegistry indexRegistry,
+ boolean allowWildcard)
{
if (indexNames == null || indexNames.isEmpty())
return Collections.emptySet();
+ boolean wildcardFound = false;
Set<IndexMetadata> indexes = new HashSet<>(indexNames.size());
for (QualifiedName indexName : indexNames)
{
- IndexMetadata index = fetchIndex(indexName, table, indexRegistry);
- indexes.add(index);
+ if (indexName.isWildcard())
+ {
+ if (wildcardFound)
+ continue;
+
+ if (!allowWildcard)
+ throw new
InvalidRequestException(WILDCARD_INCLUDED_INDEXES_ERROR);
+
+ wildcardFound = true;
+ indexes = metadata(indexRegistry.listIndexes());
+ }
+ else
+ {
+ IndexMetadata index = fetchIndex(indexName, table,
indexRegistry);
+
+ if (!wildcardFound)
+ indexes.add(index);
+ }
}
return indexes;
diff --git a/src/java/org/apache/cassandra/db/filter/IndexHints.md
b/src/java/org/apache/cassandra/db/filter/IndexHints.md
index b9eb32b4eb..e48c04bf3c 100644
--- a/src/java/org/apache/cassandra/db/filter/IndexHints.md
+++ b/src/java/org/apache/cassandra/db/filter/IndexHints.md
@@ -62,6 +62,28 @@ However, excluding indexes might make it necessary to add
`ALLOW FILTERING` to t
Indexes that are applicable to the query and that are not mentioned in these
two sets of included and excluded indexes
might or might not be used, depending on the index query planner.
+## Excluding all indexes with a wildcard
+
+Rather than enumerating every index by name, `excluded_indexes` accepts the
`*` wildcard to exclude all the
+indexes currently applicable to the queried table:
+```
+SELECT * FROM users
+ WHERE birth_year = 1981 AND country = 'FR' ALLOW FILTERING
+ WITH excluded_indexes = {*};
+```
+This is equivalent to explicitly listing every index on the table
(`birth_year_idx`, `country_idx` and
+`phone_idx` in the example schema above), and is resolved to that concrete
list of indexes by the coordinator
+before the query is sent to the replicas.
+
+The wildcard is only supported in `excluded_indexes`. Using it in
`included_indexes` (e.g.
+`included_indexes = {*}`) will be rejected, since there is no defined
semantics for "include all indexes".
+
+The wildcard can be combined with explicit index names in the same
`excluded_indexes` set (e.g.
+`excluded_indexes = {*, some_idx}`), which is redundant but not an error.
However, if `included_indexes` names
+an index that also exists on the table, combining it with `excluded_indexes =
{*}` will fail with the same
+"indexes cannot be both included and excluded" error as if that index had been
explicitly excluded, since the
+wildcard is expanded before that check is performed.
+
## Unshading queries
The presence of indexes can shade queries that used to have a different
behaviour without indexes.
diff --git
a/test/distributed/org/apache/cassandra/distributed/test/sai/IndexHintsDistributedTest.java
b/test/distributed/org/apache/cassandra/distributed/test/sai/IndexHintsDistributedTest.java
index 68e8690c58..4975c29d2e 100644
---
a/test/distributed/org/apache/cassandra/distributed/test/sai/IndexHintsDistributedTest.java
+++
b/test/distributed/org/apache/cassandra/distributed/test/sai/IndexHintsDistributedTest.java
@@ -106,6 +106,7 @@ public class IndexHintsDistributedTest extends TestBaseImpl
// test excluded indexes
assertSelect(cluster, expectedErrorMessage, select + " WITH
excluded_indexes = {legacy_idx}", eqRows);
assertSelect(cluster, expectedErrorMessage, select + " WITH
excluded_indexes = {non_analyzed_sai_idx}", eqRows);
+ assertSelect(cluster, expectedErrorMessage, select + " ALLOW
FILTERING WITH excluded_indexes = {*}", eqRows);
});
}
diff --git a/test/unit/org/apache/cassandra/db/filter/IndexHintsTest.java
b/test/unit/org/apache/cassandra/db/filter/IndexHintsTest.java
index 770cc35fe4..a913fe6082 100644
--- a/test/unit/org/apache/cassandra/db/filter/IndexHintsTest.java
+++ b/test/unit/org/apache/cassandra/db/filter/IndexHintsTest.java
@@ -40,6 +40,7 @@ import
org.apache.cassandra.cql3.statements.schema.IndexTarget;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ReadCommand;
import org.apache.cassandra.db.SinglePartitionReadCommand;
+import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.SyntaxException;
import org.apache.cassandra.index.CustomIndexTest;
@@ -432,6 +433,11 @@ public class IndexHintsTest extends CQLTester
testTransport(query + "WITH included_indexes={idx1,idx2}",
IndexHints.create(indexes(idx1, idx2), indexes()));
testTransport(query + "WITH excluded_indexes={idx1,idx2}",
IndexHints.create(indexes(), indexes(idx1, idx2)));
testTransport(query + "WITH included_indexes={idx1,idx2} AND
excluded_indexes={idx3,idx4}", IndexHints.create(indexes(idx1, idx2),
indexes(idx3, idx4)));
+
+ // the wildcard is expanded to all the indexes on the table before
it's sent over the wire, so the wire
+ // format itself is unaffected by the wildcard
+ testTransport(query + "WITH excluded_indexes={*}",
IndexHints.create(indexes(), indexes(idx1, idx2, idx3, idx4)));
+ testTransport(query + "WITH excluded_indexes={*, idx1}",
IndexHints.create(indexes(), indexes(idx1, idx2, idx3, idx4)));
}
private void testTransport(String query, IndexHints expectedHints)
@@ -1072,6 +1078,90 @@ public class IndexHintsTest extends CQLTester
assertConflictingHints("SELECT * FROM %s WHERE v3=0 WITH
included_indexes={idx3} AND excluded_indexes={idx3}", "idx3");
}
+ /**
+ * Tests the {@code *} wildcard in {@code excluded_indexes}, which should
be equivalent to explicitly excluding
+ * every index applicable to the table.
+ */
+ @Test
+ public void testWildcardExcludedIndexes()
+ {
+ createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3
int)");
+ createIndex("CREATE INDEX idx1 ON %s(v1)");
+ createIndex("CREATE CUSTOM INDEX idx2 ON %s(v2) USING
'StorageAttachedIndex'");
+ Object[] row = row(0, 0, 0, 0);
+ execute("INSERT INTO %s (k, v1, v2, v3) VALUES (0, 0, 0, 0)");
+
+ // the wildcard excludes every applicable index, same as explicitly
excluding all of them
+ assertNeedsAllowFiltering("SELECT * FROM %s WHERE v1=0 WITH
excluded_indexes={*}");
+ assertNeedsAllowFiltering("SELECT * FROM %s WHERE v2=0 WITH
excluded_indexes={*}");
+ assertNeedsAllowFiltering("SELECT * FROM %s WHERE v1=0 AND v2=0 WITH
excluded_indexes={*}");
+ assertThatIndexQueryPlanFor("SELECT * FROM %s WHERE v1=0 ALLOW
FILTERING WITH excluded_indexes={*}", row).selectsNone();
+ assertThatIndexQueryPlanFor("SELECT * FROM %s WHERE v2=0 ALLOW
FILTERING WITH excluded_indexes={*}", row).selectsNone();
+ assertThatIndexQueryPlanFor("SELECT * FROM %s WHERE v1=0 AND v2=0
ALLOW FILTERING WITH excluded_indexes={*}", row).selectsNone();
+
+ // an unrestricted column (v3) has no index anyway, so the wildcard
exclusion doesn't change anything for it
+ assertThatIndexQueryPlanFor("SELECT * FROM %s WHERE v3=0 ALLOW
FILTERING WITH excluded_indexes={*}", row).selectsNone();
+
+ // the wildcard combined with explicit names in the same set is
redundant, but not an error
+ assertThatIndexQueryPlanFor("SELECT * FROM %s WHERE v1=0 ALLOW
FILTERING WITH excluded_indexes={*, idx1}", row).selectsNone();
+ assertThatIndexQueryPlanFor("SELECT * FROM %s WHERE v1=0 ALLOW
FILTERING WITH excluded_indexes={*, idx2}", row).selectsNone();
+
+ // combining wildcard with a non-existing index name is still an error
+ String missingIndexError =
String.format(IndexHints.MISSING_INDEX_ERROR, currentTable(), "idx3");
+ assertInvalidThrowMessage(missingIndexError,
+ InvalidRequestException.class,
+ "SELECT * FROM %s WITH
excluded_indexes={idx3, *}");
+ assertInvalidThrowMessage(missingIndexError,
+ InvalidRequestException.class,
+ "SELECT * FROM %s WITH excluded_indexes={*,
idx3}");
+
+ // the wildcard is not allowed in included_indexes, alone or combined
with explicit names
+ assertInvalidThrowMessage(IndexHints.WILDCARD_INCLUDED_INDEXES_ERROR,
+ InvalidRequestException.class,
+ "SELECT * FROM %s WHERE v1=0 ALLOW FILTERING
WITH included_indexes={*}");
+ assertInvalidThrowMessage(IndexHints.WILDCARD_INCLUDED_INDEXES_ERROR,
+ InvalidRequestException.class,
+ "SELECT * FROM %s WHERE v1=0 ALLOW FILTERING
WITH included_indexes={*, idx1}");
+ assertInvalidThrowMessage(IndexHints.WILDCARD_INCLUDED_INDEXES_ERROR,
+ InvalidRequestException.class,
+ "SELECT * FROM %s WHERE v1=0 ALLOW FILTERING
WITH included_indexes={idx1, *}");
+
+ // a wildcard exclusion conflicts with an included index that exists
on the table
+ assertConflictingHints("SELECT * FROM %s WHERE v1=0 WITH
included_indexes={idx1} AND excluded_indexes={*}", "idx1");
+ assertConflictingHints("SELECT * FROM %s WHERE v2=0 WITH
included_indexes={idx2} AND excluded_indexes={*}", "idx2");
+ }
+
+ /**
+ * Tests that the size limit is also enforced on the set of excluded
indexes that results from expanding the
+ * {@code *} wildcard, since the check on the raw CQL names doesn't catch
it.
+ */
+ @Test
+ public void testWildcardExcludedIndexesOverGuardrailLimit()
+ {
+ createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3
int)");
+ createIndex("CREATE INDEX idx1 ON %s(v1)");
+ createIndex("CREATE INDEX idx2 ON %s(v2)");
+ createIndex("CREATE INDEX idx3 ON %s(v3)");
+
+ ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
+ int warn =
Guardrails.instance.getSecondaryIndexesPerTableWarnThreshold();
+ int fail =
Guardrails.instance.getSecondaryIndexesPerTableFailThreshold();
+ try
+ {
+ // lower the guardrail below the table's existing index count
+ Guardrails.instance.setSecondaryIndexesPerTableThreshold(-1, 2);
+
+ Set<QualifiedName> excluded =
Collections.singleton(QualifiedName.WILDCARD);
+ Assertions.assertThatThrownBy(() -> IndexHints.fromCQLNames(null,
excluded, cfs.metadata(), cfs.indexManager))
+ .isInstanceOf(InvalidRequestException.class)
+ .hasMessageContaining(TOO_MANY_INDEXES_ERROR + 3);
+ }
+ finally
+ {
+ Guardrails.instance.setSecondaryIndexesPerTableThreshold(warn,
fail);
+ }
+ }
+
/**
* Tests that CQL will throw an exception when trying to specify more
indexes than can fit in a short.
*/
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]